Skip to content

feat(envs): remove core envs from the manifest, load them as regular envs#10465

Open
davidfirst wants to merge 156 commits into
masterfrom
remove-core-envs-from-manifest
Open

feat(envs): remove core envs from the manifest, load them as regular envs#10465
davidfirst wants to merge 156 commits into
masterfrom
remove-core-envs-from-manifest

Conversation

@davidfirst

@davidfirst davidfirst commented Jul 2, 2026

Copy link
Copy Markdown
Member

Removes the env aspects (teambit.react/react, teambit.harmony/node, teambit.harmony/aspect, teambit.envs/env, teambit.mdx/mdx, teambit.mdx/readme) from the core manifest to slim Bit. They now act like any other env, installed from the registry.

New default env: teambit.harmony/empty-env (core). A totally empty env - no compiler, no tester, no preview, no dependency policy. Components with no env configured use it and work fully offline out of the box (add → compile no-op → tag/snap → export). Since it has no behavior, it has nothing to drift when bit itself changes - the one env that is safe to keep core (and versionless in models) forever. To get a dev experience, users configure a real env (bit create flows already do).

teambit.harmony/aspect and teambit.envs/env are removed like the rest, with zero behavior change. Their implementation is untouched (react-based, preview and all) - users get the exact released behavior after bit install (the pinned-version machinery auto-installs them). New envs are created from the bitdev env packages (bit create react-env etc.), so these built-in envs are legacy surface. The bit-aspect template and the harmony starters moved to the core generator aspect, so bit create bit-aspect and bit new keep working out of the box (the created aspect needs bit install before it loads, like any env).

Versionless by design. Config entries for the removed env ids are persisted by name, without a version - exactly as they were when core (registered as core-extension names). Keeping them versionless is deliberate on two counts. First, it keeps the env from becoming a dependency edge of its own components; otherwise an env such as react, whose dependency closure includes components that use it as their env, creates circular TS project references and breaks lane/tag builds. Second, it preserves forward compatibility: a re-tag under the new bit keeps the env id versionless, so a teammate who has not upgraded yet (whose bit still ships these as core) can import the re-tagged component and resolve the env - instead of receiving a versioned id their bit has no component for. The alternative (showing the component as modified and pinning the env on the next tag) would silently break not-yet-upgraded consumers.

Backward compatibility. Old components have the removed envs saved without a version. legacy-core-envs.ts maps them to pinned versions, applied only at the resolution/loading/install level - stored objects are never mutated. Versionless legacy ids match the env slot ignoring version, bit install auto-adds their packages, and single-instance semantics are enforced (a loaded version is reused rather than loading another copy). Not-installed legacy envs fail fast with a NonLoadedEnv issue suggesting bit install - no scope-capsule isolation in workspace context (which used to take minutes). Old components load without being reported as modified, and re-tagging keeps the env versionless - covered end-to-end by e2e/harmony/legacy-core-env-back-compat.e2e.ts, which imports a component exported by a pre-removal bit (env saved versionless) and asserts it is not modified and stays versionless after a re-tag.

Relocated core wiring: the bit aspect CLI command moved to teambit.workspace/workspace; validateBeforePersistHook moved to teambit.dependencies/dependency-resolver; the dead @teambit/legacy link is now skipped instead of crashing.

Also fixes latent issues this path exposed: versionless seeders filtering out all manifests in loadExtensionsByManifests, circular env chains causing infinite component-load recursion, versioned core-aspect ids escaping core filters and doRequire mutating shared core manifests, stack overflows from recursive graph traversal, and a spurious MissingDists issue for compiler-less envs.

Verified locally: fresh workspace (JS and TS components) - clean status in ~1s, tag/snap/export offline, bit envs/bit test graceful; this repo's workspace - status/insights/list-core clean; the seven repo components that relied on the default env are now explicitly set to the node env. bit create <template> --env <removed-env> loads the env's templates on demand from the global scope (pinned version); this path also loads the full manifest graph, and binds manifest deps of legacy envs to their pinned versions (models built when these envs were core don't list them as dependencies). The e2e setCustomEnv helper installs the env package the fixture imports (e.g. @teambit/node).

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

Copy link
Copy Markdown

PR Summary by Qodo

Load former core envs as regular registry envs with legacy version pinning

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Remove env aspects from core manifest; load them as regular, versioned env components.
• Add legacy core-env mapping to pin versions and auto-install missing packages.
• Prevent recursion/stack overflows in aspect/env loading and graph traversal paths.
Diagram

graph TD
  A["Component env id (may be versionless)"] --> C["EnvsMain (env resolution)"] --> D["Aspects loaders (ws/scope)"] --> E["InstallMain (workspace policy)"] --> F["Registry packages (@teambit/*)"]
  C --> B["legacy-core-envs.ts (pinned versions)"] --> D
  C --> G["Fallback TS compiler"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Migrate stored component env ids to include versions
  • ➕ Eliminates ongoing special-casing for versionless ids
  • ➕ Makes resolution/slot lookups simpler and more consistent
  • ➖ Mutates historical objects/models (explicitly avoided by this PR)
  • ➖ Requires migration tooling and careful rollout across scopes/workspaces
2. Resolve legacy envs to a semver range (e.g. ^1.x) instead of pinned
  • ➕ Reduces maintenance of pinned versions
  • ➕ Allows automatic uptake of compatible env fixes
  • ➖ Less deterministic; can break builds when env behavior changes
  • ➖ Harder to reproduce old snapshots and debug regressions
3. Keep env aspects in core manifest but lazy-load/bundle-split
  • ➕ Avoids registry dependency for default/basic envs
  • ➕ Minimizes behavior change in resolution codepaths
  • ➖ Does not achieve the same binary/core slimming goal
  • ➖ Still couples env release cadence to core distribution

Recommendation: The PR’s approach (treat former core envs as regular external envs, while preserving backward compatibility via a non-mutating legacy-id resolver + pinned versions) is the best tradeoff for slimming the core without breaking old components. The main follow-up to ensure long-term health is to formalize the pinned-version bump as part of the release workflow (as noted in the PR description) and consider adding a small regression test matrix around versionless legacy env ids + fallback-default-env behavior.

Files changed (15) +503 / -74

Enhancement (7) +352 / -30
environments.main.runtime.tsAdd legacy core env compatibility and fallback default env +153/-25

Add legacy core env compatibility and fallback default env

• Introduces legacy-core-env detection, slot lookups that ignore version, and special handling for versionless legacy ids. Adds a minimal fallback default env (with TS transpiler) to keep commands working before env installation and prevents self-referential env component loading loops.

scopes/envs/envs/environments.main.runtime.ts

fallback-typescript-compiler.tsAdd minimal transpile-only TypeScript compiler for fallback env +43/-0

Add minimal transpile-only TypeScript compiler for fallback env

• Implements a lightweight TypeScript transpiler (no type-checking) used by the fallback default env to produce requirable dists in capsules when the real env is not installed/loaded yet.

scopes/envs/envs/fallback-typescript-compiler.ts

index.tsExport legacy core env utilities from envs public API +7/-0

Export legacy core env utilities from envs public API

• Re-exports helper functions for legacy core env identification, pinning, package naming, and id resolution so workspace/scope/install hosts can share the same compatibility logic.

scopes/envs/envs/index.ts

legacy-core-envs.tsDefine pinned versions and helpers for legacy core env ids +59/-0

Define pinned versions and helpers for legacy core env ids

• Adds a central mapping from legacy core env ids to pinned versions plus helpers to resolve versionless ids and derive registry package names. Includes a list of older removed env ids to suppress invalid-config errors even without a pinned package.

scopes/envs/envs/legacy-core-envs.ts

scope-aspects-loader.tsNormalize legacy core env ids to pinned versions in scope loading +10/-1

Normalize legacy core env ids to pinned versions in scope loading

• Resolves versionless legacy core env ids to pinned versions before importing/loading, enabling external env loading from registry. Improves core-aspect filtering to exclude core aspects even when requested with versions (dependency-induced).

scopes/scope/scope/scope-aspects-loader.ts

install.main.runtime.tsAuto-install legacy core env packages via workspace policy pinning +42/-1

Auto-install legacy core env packages via workspace policy pinning

• Adds legacy core envs used by components (without versions) to the workspace policy using pinned versions and derived @teambit/* package names. Extends missing-env package resolution to install pinned legacy env packages when env ids are versionless and not in workspace.

scopes/workspace/install/install.main.runtime.ts

workspace-component-loader.tsEnsure legacy core env extensions and default env participate in load groups +38/-3

Ensure legacy core env extensions and default env participate in load groups

• Collects name-only legacy env extensions so they are resolved and loaded before dependent components, and ensures DEFAULT_ENV is included for components without explicit env configuration. Treats legacy core env components as env aspects even when env-data is computed via fallback env.

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts

Bug fix (5) +149 / -23
dev-files.main.runtime.tsSkip env manifest detection for legacy core env ids +3/-0

Skip env manifest detection for legacy core env ids

• Avoids fetching legacy core env components solely to look for env.jsonc, since old-style envs intentionally lack it. Keeps core/legacy envs out of dev-files env-manifest logic for faster/safer resolution.

scopes/component/dev-files/dev-files.main.runtime.ts

dependency-resolver.main.runtime.tsHarden env-root module resolution and legacy env policy handling +19/-4

Harden env-root module resolution and legacy env policy handling

• Guards getPackageDirInEnvRoot against cases where component env cannot be determined, falling back to root node_modules. Extends legacy peer-policy inclusion and env.jsonc detection to treat legacy core envs like core envs (no env.jsonc fetch).

scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts

aspect-loader.main.runtime.tsAvoid mutating shared core manifests when requiring aspects +7/-0

Avoid mutating shared core manifests when requiring aspects

• Prevents overriding manifest.id when require() resolves to a core aspect module, avoiding shared-object mutation that can break core aspect resolution (e.g. accidentally searching core ids with a version suffix).

scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts

workspace-aspects-loader.tsLoad legacy envs reliably and guard against circular/aspect-graph recursion +93/-11

Load legacy envs reliably and guard against circular/aspect-graph recursion

• Adds versionless-legacy env matching when checking whether aspects are already loaded, resolves pinned versions for non-workspace legacy envs, and includes resolved ids as seeders to prevent manifest filtering bugs. Introduces in-flight load tracking to break circular env chains and replaces recursive predecessor traversal with safer inEdges-based logic to avoid stack overflows on large graphs.

scopes/workspace/workspace/workspace-aspects-loader.ts

workspace.tsTrack in-flight aspect loads and avoid recursive dependent traversal +27/-8

Track in-flight aspect loads and avoid recursive dependent traversal

• Adds a workspace-level inFlightAspectsLoads set used to prevent circular env/aspect load chains. Reworks getDependentsIds to iterative traversal to avoid maximum call stack errors, and skips misconfigured-env warnings for legacy core env ids.

scopes/workspace/workspace/workspace.ts

Refactor (1) +1 / -2
ui.main.runtime.tsDrop unused AspectMain dependency from UI deps tuple +1/-2

Drop unused AspectMain dependency from UI deps tuple

• Simplifies UI aspect dependency typing by removing an unused AspectMain type from UIDeps.

scopes/ui-foundation/ui/ui.main.runtime.ts

Tests (1) +1 / -7
core-aspects-ids.jsonUpdate core aspect id list to exclude former core envs +1/-7

Update core aspect id list to exclude former core envs

• Removes env aspect ids from the core-aspects test fixture list to reflect the slimmer core manifest set.

scopes/harmony/testing/load-aspect/core-aspects-ids.json

Other (1) +0 / -12
manifests.tsRemove env aspects from core manifests map +0/-12

Remove env aspects from core manifests map

• Stops bundling former core env aspects (node/react/mdx/readme/env/aspect-related) as core manifests, aligning with the new model where they are installed and loaded as regular env components.

scopes/harmony/bit/manifests.ts

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

qodo-free-for-open-source-projects Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. In-flight load returns early 🐞 Bug ☼ Reliability
Description
WorkspaceAspectsLoader.loadAspectsWithSpan() drops IDs already present in
workspace.inFlightAspectsLoads and returns immediately when all requested IDs are "in-flight",
without waiting for the original load to finish. Concurrent component/aspect loads can therefore
proceed while a required aspect/env is still loading, causing timing-dependent failures or fallback
behavior.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R125-137]

+    // break circular env chains - if an aspect is already in the process of loading (a parent
+    // call in the current chain), don't try to load it again.
+    const [inFlightIds, idsToLoad] = partition(notLoadedIds, (id) =>
+      this.workspace.inFlightAspectsLoads.has(aspectLoadInFlightKey(id))
+    );
+    if (inFlightIds.length) {
+      this.logger.debug(`${loggerPrefix} skipping aspects that are already loading: ${inFlightIds.join(', ')}`);
+    }
+    notLoadedIds = idsToLoad;
  if (!notLoadedIds.length) {
    span.setAttribute('alreadyLoaded', true);
    return [];
  }
Evidence
The loader explicitly filters out IDs already present in workspace.inFlightAspectsLoads and
returns early when nothing remains to load, but inFlightAspectsLoads is only a Set (no stored
Promise) so there is nothing to await. Component loading uses concurrency (pMap with a concurrency
limit), making overlapping aspect loads plausible and turning the early return into a real race.

scopes/workspace/workspace/workspace-aspects-loader.ts[121-144]
scopes/workspace/workspace/workspace.ts[210-214]
scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[785-803]

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

## Issue description
`WorkspaceAspectsLoader.loadAspectsWithSpan()` treats `workspace.inFlightAspectsLoads` as a dedupe/cycle-breaker, but when all requested aspect IDs are already marked in-flight it returns `[]` immediately. Since `inFlightAspectsLoads` only stores string keys (no Promise to await), concurrent callers that request the same aspect can resolve before the aspect is actually loaded, leading to racey missing-aspect behavior.
### Issue Context
- This PR introduces `workspace.inFlightAspectsLoads` specifically to break circular env chains. That use-case should remain (avoid deadlocks/infinite recursion), but concurrent *non-reentrant* calls should join/await the in-flight load instead of returning early.
### Fix Focus Areas
- scopes/workspace/workspace/workspace.ts[210-214]
- scopes/workspace/workspace/workspace-aspects-loader.ts[121-145]
### Suggested fix approach
1. Replace/augment `Workspace.inFlightAspectsLoads` (currently `Set<string>`) with an awaitable structure, e.g. `Map<string, Promise<string[]>>` (or `Promise<void>`).
2. When `loadAspectsWithSpan()` detects an ID already in-flight:
- **If it’s re-entrant in the same call chain (cycle case)**: keep current behavior (skip) to avoid deadlock.
- **If it’s a concurrent call (not re-entrant)**: await the existing in-flight Promise for that key, then proceed/return.
Practical way to distinguish re-entrant vs concurrent: use `AsyncLocalStorage` (or an explicit per-call “load session” token) to track the current call-chain keys.
3. Ensure map entries are cleaned up in `finally` to avoid memory leaks and stuck states on errors.
4. Add a unit/e2e-style test that triggers two concurrent `workspace.loadAspects([sameId])` calls and asserts the second does not resolve before the aspect becomes available in harmony.

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


2. Core env component missing 🐞 Bug ☼ Reliability
Description
EnvsMain.getEnvComponent() throws if getEnvComponentByEnvId() returns undefined, but
getEnvComponentByEnvId() intentionally returns undefined for core envs when the env component
doesn't exist anywhere. Components using the new default core env (empty-env) can hit this and fail
commands that call getEnvComponent for metadata despite the env being loaded.
Code

scopes/envs/envs/environments.main.runtime.ts[R635-647]

const envId = this.getEnvId(component);
-    return this.getEnvComponentByEnvId(envId, component.id.toString());
+    // the env of the component might be the component itself. e.g. an env whose own env was not
+    // loaded falls back to itself. getting it from the host here would cause an infinite loop of
+    // loading it.
+    if (envId.split('@')[0] === component.id.toStringWithoutVersion()) {
+      return component;
+    }
+    const envComponent = await this.getEnvComponentByEnvId(envId, component.id.toString());
+    if (!envComponent) {
+      throw new BitError(
+        `unable to get the env component of ${component.id.toString()}, its env ${envId} was not found`
+      );
+    }
Evidence
The diff changes getEnvComponentByEnvId to return undefined for core envs when
resolving/fetching fails, but getEnvComponent now throws when it receives undefined,
contradicting the intended best-effort core-env behavior.

scopes/envs/envs/environments.main.runtime.ts[635-648]
scopes/envs/envs/environments.main.runtime.ts[654-665]

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

## Issue description
`EnvsMain.getEnvComponent()` assumes `getEnvComponentByEnvId()` always returns a `Component`, but `getEnvComponentByEnvId()` now returns `undefined` for core envs when their env-component cannot be resolved/fetched. This makes flows that query the env-component crash for components using the default core env (`teambit.harmony/empty-env`).
### Issue Context
Core envs are loaded from the binary and may not have an exported component in the workspace/scope. The new code explicitly makes `getEnvComponentByEnvId()` best-effort for core envs.
### Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[635-648]
### Suggested fix
In `getEnvComponent()`, avoid throwing when the env is core (or when `getEnvComponentByEnvId()` returns undefined). Options:
- Return `component` (or a safe placeholder) for core envs when env-component metadata is unavailable.
- Or change `getEnvComponent()` return type to `Promise<Component | undefined>` and update callers to tolerate `undefined`.
Pick the approach that matches current call sites, but ensure core-env users don’t hard-fail solely due to missing env-component objects.

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


3. Legacy env preview crash ✓ Resolved 🐞 Bug ☼ Reliability
Description
PreviewMain.doesScaling() and isSupportSkipIncludes() still call envs.getEnvComponent() without
guarding legacy core envs that may not be installed yet, even though the new helper explicitly notes
getEnvComponent can throw in that case. This can break Preview GraphQL queries
(isScaling/skipIncludes) for components using removed core envs until after a successful "bit
install".
Code

scopes/preview/preview/preview.main.runtime.ts[R327-338]

+  /**
+   * envs that used to be core aspects keep their core preview behavior. also, they may not be
+   * installed yet, in which case fetching their env component (getEnvComponent) throws - and this
+   * runs during component load.
+   */
+  private isUsingCoreOrLegacyCoreEnv(component: Component): boolean {
+    return this.envs.isUsingCoreEnv(component) || this.isUsingLegacyCoreEnv(component);
+  }
+
+  private isUsingLegacyCoreEnv(component: Component): boolean {
+    return this.envs.isLegacyCoreEnv(this.envs.getEnvId(component));
+  }
Evidence
The PR adds an explicit comment/helper stating legacy core envs may not be installed and that
getEnvComponent() throws, but other preview methods still call getEnvComponent() unconditionally;
GraphQL resolvers invoke those methods directly, so the exception can surface to users.

scopes/preview/preview/preview.main.runtime.ts[327-338]
scopes/preview/preview/preview.main.runtime.ts[601-633]
scopes/preview/preview/preview.main.runtime.ts[646-653]
scopes/preview/preview/preview.graphql.ts[36-55]

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

## Issue description
`PreviewMain` now recognizes that legacy core envs may not be installed and that calling `envs.getEnvComponent()` can throw for them, but this guard is only applied to some preview calculations. `doesScaling()` and `isSupportSkipIncludes()` still unconditionally call `getEnvComponent()`, which can cause GraphQL preview resolvers to throw for components using legacy core envs before `bit install`.
## Issue Context
- `Preview` GraphQL resolvers call `PreviewMain.doesScaling()` and `PreviewMain.isSupportSkipIncludes()`.
- Legacy core envs are intentionally allowed to be configured versionless and may be absent until installation.
## Fix Focus Areas
- scopes/preview/preview/preview.main.runtime.ts[601-633]
- scopes/preview/preview/preview.main.runtime.ts[646-653]
- scopes/preview/preview/preview.main.runtime.ts[327-338]
## Expected fix
- Update `doesScaling()` to avoid calling `getEnvComponent()` when the component uses a legacy core env (similar to `calcDoesScalingForComponent()`), returning the appropriate safe default.
- Update `isSupportSkipIncludes()` to avoid calling `getEnvComponent()` for legacy core envs as well (and decide on the desired default behavior, likely matching core-env behavior or returning a conservative default).
- Optionally, add a shared helper usage (`isUsingCoreOrLegacyCoreEnv`) in these methods for consistency.

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


View more (2)
4. Missing await in hasId 🐞 Bug ≡ Correctness
Description
InstallMain.addUsedLegacyCoreEnvsToWorkspacePolicy calls this.workspace.hasId(...) without awaiting
the Promise, so the condition is always truthy and legacy core env packages will never be added to
the workspace policy, breaking auto-install of removed core envs.
Code

scopes/workspace/install/install.main.runtime.ts[R915-926]

+      // if the env exists in the workspace, it is loaded from the workspace, no need to install it
+      if (this.workspace.hasId(envId, { ignoreVersion: true })) return;
+      rootPolicy.add(
+        {
+          dependencyId: getLegacyCoreEnvPackageName(envIdStr),
+          value: {
+            version,
+          },
+          lifecycleType: 'runtime',
+        },
+        // If it's already exist from the root, take the version from the root policy
+        { skipIfExisting: true }
Evidence
The diff shows a Promise-returning call (workspace.hasId) being used as a boolean without
awaiting, which will always evaluate as truthy and short-circuit the policy add. The same file uses
await this.workspace.hasId(...) elsewhere, confirming it is async and must be awaited.

scopes/workspace/install/install.main.runtime.ts[907-927]
scopes/workspace/install/install.main.runtime.ts[1152-1156]

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

## Issue description
`addUsedLegacyCoreEnvsToWorkspacePolicy()` uses `this.workspace.hasId(...)` in an `if` without `await`. Since `hasId()` returns a Promise, the condition is always truthy and the function returns early, preventing legacy core env packages (e.g. `@teambit/node`, `@teambit/react`) from being added to the workspace policy.
### Issue Context
This code path is responsible for ensuring envs that used to be core aspects (stored versionless in old components) are installed from the registry using pinned versions.
### Fix Focus Areas
- scopes/workspace/install/install.main.runtime.ts[915-926]
### Suggested change
- Change `if (this.workspace.hasId(envId, { ignoreVersion: true })) return;` to `if (await this.workspace.hasId(envId, { ignoreVersion: true })) return;` (or equivalent), keeping the existing intent to skip installing when the env exists in the workspace.

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


5. TS main forced to dist 🐞 Bug ≡ Correctness
Description
NodeModuleLinker rewrites package.json.main for TS/TSX/MTS/CTS entries to dist/*.js without
checking whether the component actually produces dists. With the new default
teambit.harmony/empty-env providing no compiler (components used as-source), this can cause linked
workspace packages to point at a non-existent entry file and break module resolution.
Code

scopes/workspace/modules/node-modules-linker/node-modules-linker.ts[R247-256]

+    // never persist a source file as the package main. the transformers above normally set the
+    // compiled main, but when they have no env data (e.g. the early linking step of "bit install"
+    // runs before components are loaded), the main stays the component's source main-file.
+    // requiring such a package crashes on node 22 (require(esm) of a .ts entry with extensionless
+    // relative imports), breaking every subsequent component load. point it at the compiled dist
+    // location instead.
+    const mainFile = packageJson.packageJsonObject.main;
+    if (typeof mainFile === 'string' && /\.(ts|tsx|jsx|mts|cts)$/.test(mainFile) && !mainFile.endsWith('.d.ts')) {
+      packageJson.packageJsonObject.main = `dist/${mainFile.replace(/\.(ts|tsx|jsx|mts|cts)$/, '.js')}`;
+    }
Evidence
The linker rewrite is unconditional for TS-like main values, while empty-env is explicitly
compiler-less and the compiler subsystem treats such envs as having no dists (used as-source),
making dist/... an invalid entrypoint for those components.

scopes/workspace/modules/node-modules-linker/node-modules-linker.ts[247-256]
scopes/harmony/empty-env/empty-env.main.runtime.ts[8-15]
scopes/compilation/compiler/compiler.main.runtime.ts[118-126]

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

## Issue description
`NodeModuleLinker` unconditionally rewrites a TypeScript-family `package.json.main` (e.g. `index.ts`) to `dist/index.js`. For components using the new default `teambit.harmony/empty-env` (no compiler/dists; used as-source), this produces a `main` that often does not exist, breaking consumers that resolve the package via `main`.
### Issue Context
The PR introduces `empty-env` as the default env and explicitly states it provides no compiler and components are used as-source.
### Fix Focus Areas
- scopes/workspace/modules/node-modules-linker/node-modules-linker.ts[247-256]
### Suggested fix
Adjust the rewrite logic so it only forces `main` to `dist/*` when it is actually appropriate, for example:
- Only rewrite when the component is an aspect/env (i.e. a runtime extension that must be require-able by Node), OR when a compiler is known to exist for the component, OR when the target `dist/...` file already exists.
- Otherwise keep the source `main` (or avoid setting it to a non-existent dist entry) to preserve source-based workflows under `empty-env`.
This keeps the Node-22 workaround for require-able extensions while avoiding breaking default (compiler-less) components.

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



Remediation recommended

6. In-flight key mismatch 🐞 Bug ☼ Reliability ⭐ New
Description
WorkspaceAspectsLoader tracks in-flight loads using the pre-resolved aspect id string, but then
resolves ids to versioned ComponentIDs before loading; versionless and versioned requests for the
same aspect won’t match and can run duplicate concurrent loads. This undermines the intended
cycle/dedup guard and can re-run non-idempotent initialization work in parallel.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R127-144]

+    const [inFlightIds, idsToLoad] = partition(notLoadedIds, (id) =>
+      this.workspace.inFlightAspectsLoads.has(aspectLoadInFlightKey(id))
+    );
+    if (inFlightIds.length) {
+      this.logger.debug(`${loggerPrefix} skipping aspects that are already loading: ${inFlightIds.join(', ')}`);
+    }
+    notLoadedIds = idsToLoad;
    if (!notLoadedIds.length) {
      span.setAttribute('alreadyLoaded', true);
      return [];
    }
+    const inFlightAdded = notLoadedIds.map(aspectLoadInFlightKey);
+    inFlightAdded.forEach((id) => this.workspace.inFlightAspectsLoads.add(id));
+    try {
+      return await this.loadAspectsAfterInFlightCheck(notLoadedIds, span, neededFor, mergedOpts, loggerPrefix);
+    } finally {
+      inFlightAdded.forEach((id) => this.workspace.inFlightAspectsLoads.delete(id));
+    }
Evidence
The loader adds/checks in-flight keys from notLoadedIds before resolution, then resolves those ids
to componentIds (likely versioned) and proceeds to load using the resolved ids. For non-legacy
aspects, aspectLoadInFlightKey() keeps the full id (including version when present), so id vs
id@version won’t collide in the in-flight Set. Versionless env references are a supported input
per EnvsMain’s own comments.

scopes/workspace/workspace/workspace-aspects-loader.ts[121-160]
scopes/workspace/workspace/workspace-aspects-loader.ts[168-197]
scopes/envs/envs/legacy-core-envs.ts[57-66]
scopes/envs/envs/environments.main.runtime.ts[292-297]

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

### Issue description
`WorkspaceAspectsLoader` uses `workspace.inFlightAspectsLoads` to dedupe/break re-entrant loads, but it checks/adds keys based on the *raw requested ids* (`notLoadedIds`). Later in the same flow it resolves those ids to canonical `ComponentID`s (often versioned) and loads by those resolved ids.

If the same aspect/env is requested concurrently once as `scope/name` and once as `scope/name@x.y.z`, the in-flight Set will treat them as different keys (for non-legacy aspects), allowing duplicate concurrent loads.

### Issue Context
The codebase explicitly supports env/aspect references without a version in some flows (e.g. envs-config / legacy ids), so versionless inputs are expected.

### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[121-203]
- scopes/envs/envs/legacy-core-envs.ts[57-66]

### Suggested fix
Canonicalize the in-flight key to match the canonical id used for loading:
- Resolve ids (or at least normalize) **before** `inFlightAspectsLoads.has/add/delete`.
- For versionless requested ids, consider using `id.split('@')[0]` as the in-flight key (or store *both* the versionless key and the resolved versioned key) so that later versioned requests map to the same in-flight entry.
- Keep legacy-core-env single-instance semantics intact (current `aspectLoadInFlightKey()` behavior).

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


7. Config-only env ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
EnvsMain.calculateEnvId ignores teambit.envs/envs.config.env when the component has no matching env
aspect entry, so env reporting and NonLoadedEnv issue detection can silently fall back to the
default env. This affects components whose env is configured only via envs config (e.g. generated
env templates), leading to wrong "bit show" output and missed remediation until later failures.
Code

scopes/envs/envs/environments.main.runtime.ts[R538-554]

+    let envIdFromEnvData = this.getEnvIdFromEnvsData(component);
+    if (!envIdFromEnvData) {
+      // the envs data may not exist when the component was loaded without executing the load
+      // slot (e.g. when it's loaded from the scope during aspects resolution). fall back to the
+      // env from the config.
+      const envIdFromConfig = this.getEnvIdFromEnvsConfig(component);
+      if (envIdFromConfig) {
+        const configEnvId = ComponentID.fromString(envIdFromConfig);
+        const configEnvIdWithoutVersion = configEnvId.toStringWithoutVersion();
+        // prefer the version from the component's own aspect entry (resolveEnv below anchors to
+        // it, and it stays up to date during tagging). when no such entry exists, keep an
+        // explicitly configured version rather than dropping it and matching an arbitrary loaded
+        // version of the env.
+        envIdFromEnvData = this.resolveEnv(component, configEnvIdWithoutVersion)
+          ? configEnvIdWithoutVersion
+          : configEnvId.toString();
+      }
Evidence
The workspace component loader explicitly notes and handles the case where an env is configured only
via teambit.envs/envs: { env: ... } without an extension entry. However,
EnvsMain.calculateEnvId() only returns the env-from-config when it finds a matching aspect entry;
otherwise it searches only the aspect entries (which omit config-only envs) and falls back to the
default env. addNonLoadedEnvAsComponentIssues() uses calculateEnvId(), so it can miss reporting
the real env as not loaded.

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[245-283]
scopes/envs/envs/environments.main.runtime.ts[795-839]
scopes/envs/envs/environments.main.runtime.ts[1342-1366]
scopes/envs/envs/env.fragment.ts[5-33]

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

## Issue description
`EnvsMain.calculateEnvId()` does not return `teambit.envs/envs.config.env` unless there is a matching env aspect entry on the component. For components whose env is configured only via envs config (no env-aspect entry), `calculateEnvId()` falls back to `findFirstEnv()` over the component's aspects entries (which won’t include the configured env) and then to the default env.
Because `addNonLoadedEnvAsComponentIssues()` and the `EnvFragment` rely on `calculateEnvId()`, this can cause:
- wrong env shown in `bit show`
- missing `NonLoadedEnv` / `ExternalEnvWithoutVersion` issues for the actually-configured env
## Issue Context
The workspace loader explicitly supports envs configured only via `teambit.envs/envs: { env: ... }` without an extension entry (e.g. generated env templates). In that scenario, `calculateEnvId()` should still return the configured env id (and let later resolution/version anchoring do its job).
## Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[795-839]
- scopes/envs/envs/environments.main.runtime.ts[1342-1366]
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[273-283]
## Suggested fix
Update `calculateEnvId()` so that when `envIdFromEnvsConfig` exists and no matching env aspect entry is found, it returns the configured env id instead of proceeding to `findFirstEnv()`.
Example logic (preserve explicit versions if present):
- keep the existing "matched aspect entry" branch (it can return a versioned `ComponentID`)
- otherwise, if `envIdFromEnvsConfig` is set, return `ComponentID.fromString(envIdFromEnvsConfig)`
Consider adding an e2e/unit test for a component where only `teambit.envs/envs.config.env` is set (no env-aspect entry) and assert:
- `calculateEnvId()` returns that configured env
- `bit show` displays it
- `NonLoadedEnv` is produced when the env is not installed/loaded

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


8. Yarn locator mutated post-setup 🐞 Bug ☼ Reliability
Description
YarnPackageManager.createWorkspace mutates
Workspace.locator/anchoredDescriptor/anchoredLocator after await ws.setup() using
@ts-ignore, which can violate workspace lifecycle invariants and make workspace identity/state
brittle. This risks subtle install/resolution failures if Yarn’s setup phase caches identity derived
from the original locator.
Code

scopes/dependencies/yarn/yarn.package-manager.ts[R231-245]

+    // when the project has no package.json name, yarn synthesizes the workspace ident from the
+    // directory basename. the ".bit_roots" dir of an env installed from the registry is named
+    // with its version (e.g. "my-org_my-env@1.0.0"), so the synthesized ident holds an extra "@".
+    // such an ident doesn't survive yarn's locator stringify/parse round-trip (parsing splits on
+    // the first "@", leaving a protocol-less reference), which fails the resolution of the
+    // project's file: dependencies with "isn't supported by any available fetcher".
+    if (ws.locator.name.includes('@')) {
+      const sanitizedIdent = structUtils.makeIdent(ws.locator.scope, ws.locator.name.replace(/@/g, '_'));
+      // @ts-ignore: It's ok to initialize it now, even if it's readonly (setup was just called)
+      ws.locator = structUtils.makeLocator(sanitizedIdent, ws.reference);
+      // @ts-ignore: It's ok to initialize it now, even if it's readonly (setup was just called)
+      ws.anchoredDescriptor = structUtils.makeDescriptor(ws.locator, `${WorkspaceResolver.protocol}${ws.relativeCwd}`);
+      // @ts-ignore: It's ok to initialize it now, even if it's readonly (setup was just called)
+      ws.anchoredLocator = structUtils.makeLocator(ws.locator, `${WorkspaceResolver.protocol}${ws.relativeCwd}`);
+    }
Evidence
The new code changes readonly identity fields after ws.setup() (via @ts-ignore). The same module
explicitly states these overrides are safe when setup is called right after construction, implying
the identity should be set during the setup lifecycle rather than after it.

scopes/dependencies/yarn/yarn.package-manager.ts[229-245]
scopes/dependencies/yarn/yarn.package-manager.ts[273-288]

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

## Issue description
`createWorkspace()` calls `await ws.setup()` and only then mutates Yarn Workspace identity fields (`locator`, `anchoredDescriptor`, `anchoredLocator`) using `@ts-ignore`. This is brittle because other code in the same file documents these overrides as safe specifically when done before/around setup, and post-setup mutation can leave internal state inconsistent.
## Issue Context
This is trying to handle `.bit_roots` directories that contain `@` in their basename, which breaks Yarn locator stringify/parse round-trips.
## Fix Focus Areas
- scopes/dependencies/yarn/yarn.package-manager.ts[229-245]
- scopes/dependencies/yarn/yarn.package-manager.ts[273-288]
## Suggested fix
Move the sanitization so the workspace ident/locator is correct *before* `ws.setup()` performs any initialization that may depend on it. Options:
1) Set a safe `ws.manifest.name` (or otherwise inject a safe ident) before calling `ws.setup()` so Yarn doesn’t synthesize a locator from the directory basename.
2) Alternatively, refactor to a dedicated helper similar to `overrideInternalWorkspaceParams()` that is invoked prior to setup (or ensure setup is re-run after changing identity, if Yarn supports it).
Ensure all identity-related fields that Yarn relies on remain internally consistent.

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


View more (5)
9. Versionless env precedence weak 🐞 Bug ≡ Correctness
Description
When multiple versions of the same env are registered, getEnvFromSlotIgnoreVersion() resolves a
versionless env ID by sorting and picking one; for any non-semver version it falls back to lexical
localeCompare (explicitly including snap hashes). This deterministic but weak precedence rule can
cause versionless references to bind to a snap-hash env over a semver env (or vice versa) based on
string ordering rather than an intentional policy, changing env behavior unexpectedly.
Code

scopes/envs/envs/environments.main.runtime.ts[R300-313]

+  private getEnvFromSlotIgnoreVersion(envIdMaybeVersioned: string): EnvDefinition | undefined {
+    const envIdWithoutVersion = envIdMaybeVersioned.split('@')[0];
+    const matches = this.envSlot.toArray().filter(([envId]) => envId.split('@')[0] === envIdWithoutVersion);
+    if (!matches.length) return undefined;
+    if (matches.length > 1) {
+      // sort descending by the version part so the pick is deterministic. use semver precedence
+      // when possible (string collation misorders prereleases, e.g. rc.10 vs rc.2), fall back to
+      // string comparison for non-semver versions (snap hashes)
+      matches.sort(([a], [b]) => {
+        const aVersion = a.split('@')[1] || '';
+        const bVersion = b.split('@')[1] || '';
+        if (semver.valid(aVersion) && semver.valid(bVersion)) return semver.rcompare(aVersion, bVersion);
+        return bVersion.localeCompare(aVersion, undefined, { numeric: true });
+      });
Evidence
The env-slot lookup explicitly falls back to string comparison for non-semver versions (including
snap hashes) and is used to resolve any versionless envId. The codebase treats snap hashes as
versions, so this non-semver path is realistic.

scopes/envs/envs/environments.main.runtime.ts[292-326]
scopes/envs/envs/environments.main.runtime.ts[1199-1212]
components/legacy/scope/repositories/sources.ts[151-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
`getEnvFromSlotIgnoreVersion()` picks a single env instance for versionless references by sorting matched registered IDs. If both versions are not valid semver, it falls back to lexical sorting of the version suffix, even though versions can be snap hashes.
This can cause a versionless env reference to select a snap-hash version (or some other non-semver) over a semver release just because the hash string sorts higher, which is not a meaningful precedence policy.
## Issue Context
- The function is used by `getEnvDefinitionByStringId()` for any envId without '@', so versionless references are common.
- Bit supports snap-hash versions, so non-semver versions are expected.
## Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[292-326]
- scopes/envs/envs/environments.main.runtime.ts[1199-1212]
## Suggested fix
Adjust the precedence policy when multiple versions exist:
- If there is at least one semver version among matches, prefer the highest semver version over any non-semver versions.
- If there are only non-semver versions (snap hashes), avoid pretending one is "highest"; instead either:
- require an explicit version (fail with an actionable error), or
- pick a stable rule tied to the requesting context (e.g., prefer already-loaded exact match / prefer the version referenced by the component’s aspect entry), and keep warning.
This makes the chosen env instance policy-driven rather than hash-string-driven.

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


10. In-flight aspects not awaited 🐞 Bug ☼ Reliability
Description
ScopeAspectsLoader.getManifestsGraphRecursively filters out IDs already present in
ScopeMain.inFlightAspectLoads and proceeds without waiting for those loads to finish, so concurrent
callers can end up loading an incomplete manifest graph. This can intermittently break aspect/env
loading (missing manifests/plugins) depending on timing, because loadAspects immediately loads only
the returned manifests.
Code

scopes/scope/scope/scope-aspects-loader.ts[R128-151]

+    let nonVisitedId = ids.filter((id) => !visited.includes(id));
+    // break re-entrant load cycles: an aspect's manifest deps (or its env) may lead back to an
+    // aspect that a parent call in the current chain is already loading - the nested calls start
+    // with a fresh `visited` list, so without this guard such cycles never converge (same
+    // approach as the workspace-side inFlightAspectsLoads).
+    const [inFlightIds, idsToLoad] = partition(nonVisitedId, (id) =>
+      this.scope.inFlightAspectLoads.has(aspectLoadInFlightKey(id))
+    );
+    if (inFlightIds.length) {
+      this.logger.debug(
+        `getManifestsGraphRecursively, skipping aspects that are already loading: ${inFlightIds.join(', ')}`
+      );
+    }
+    nonVisitedId = idsToLoad;
if (!nonVisitedId.length) {
return { manifests: [], potentialPluginsIds: [] };
}
+    const inFlightAdded = nonVisitedId.map(aspectLoadInFlightKey);
+    inFlightAdded.forEach((id) => this.scope.inFlightAspectLoads.add(id));
+    try {
+      return await this.getManifestsGraphAfterInFlightCheck(nonVisitedId, visited, throwOnError, lane, opts);
+    } finally {
+      inFlightAdded.forEach((id) => this.scope.inFlightAspectLoads.delete(id));
+    }
Evidence
The scope loader explicitly drops in-flight IDs and never re-adds/awaits them, while
ScopeAspectsLoader is created per call and the in-flight set is shared on ScopeMain—so concurrent
calls can observe each other’s in-flight markers and skip work. Also, loadAspects immediately loads
only the manifests returned by getManifestsGraphRecursively, with no additional waiting for skipped
IDs.

scopes/scope/scope/scope-aspects-loader.ts[115-151]
scopes/scope/scope/scope-aspects-loader.ts[46-69]
scopes/scope/scope/scope.main.runtime.ts[205-210]
scopes/scope/scope/scope.main.runtime.ts[431-440]

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

## Issue description
`ScopeAspectsLoader.getManifestsGraphRecursively()` currently **skips** aspects whose `aspectLoadInFlightKey(id)` is already present in `scope.inFlightAspectLoads`. Because `ScopeAspectsLoader` is instantiated per call while `inFlightAspectLoads` is shared on `ScopeMain`, this can happen across concurrent invocations. The skipping invocation then returns a manifest list that may be missing required aspects, and `loadAspects()` proceeds to `loadExtensionsByManifests()` without any later synchronization.
### Issue Context
The current approach breaks re-entrant cycles, but it also acts as cross-call deduplication without awaiting the original in-flight work.
### Fix Focus Areas
- scopes/scope/scope/scope-aspects-loader.ts[115-151]
### Implementation guidance
- Replace the shared `inFlightAspectLoads: Set<string>` concept with (or augment it by) a shared **in-flight map** keyed by `aspectLoadInFlightKey(id)` that stores a `Promise` representing the load/graph computation for that key.
- When encountering an ID that is already in-flight:
- If it’s a true *re-entrant cycle within the same call stack*, keep the current cycle-breaking behavior (skip to avoid deadlock). This can be tracked with a **stack-local** `visiting`/`callStack` set passed through recursion.
- Otherwise (another concurrent invocation), **await/join** the existing promise so the caller does not proceed with a partial graph.
- Ensure the joined result contributes the needed manifest IDs to the current call’s returned manifests (or at least ensures the aspect is loaded before returning).
- Add a targeted test that runs two concurrent `scope.loadAspects([...sameId...])` calls and asserts both calls end with the aspect loaded and manifest graph complete.

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


11. Fallback compiler dist mismatch 🐞 Bug ≡ Correctness
Description
The fallback TypeScript compiler's transpileFile outputs paths without the dist prefix while its
getDistPathBySrcPath includes a dist prefix, creating inconsistent dist mapping between build-time
and runtime resolution. This can lead to incorrect path computations when other parts of the system
rely on getDistPathBySrcPath for files produced via transpileFile.
Code

scopes/envs/envs/fallback-typescript-compiler.ts[R34-57]

+  const replaceFileExtToJs = (filePath: string): string => filePath.replace(/\.(ts|tsx|jsx|mts|cts)$/, '.js');
+  return {
+    id: 'teambit.envs/envs/fallback-typescript-compiler',
+    displayName: 'Fallback TypeScript',
+    distDir: 'dist',
+    shouldCopyNonSupportedFiles: true,
+    displayConfig: () => JSON.stringify(compilerOptions, null, 2),
+    version: () => ts.version as string,
+    isFileSupported: (filePath: string) =>
+      supportedExtensions.some((ext) => filePath.endsWith(ext)) &&
+      !['.d.ts', '.d.mts', '.d.cts'].some((ext) => filePath.endsWith(ext)),
+    getDistPathBySrcPath: (srcPath: string) => `dist/${replaceFileExtToJs(srcPath)}`,
+    transpileFile: (fileContent: string, options: { componentDir: string; filePath: string }) => {
+      const result = ts.transpileModule(fileContent, {
+        compilerOptions,
+        fileName: options.filePath,
+      });
+      return [
+        {
+          outputText: result.outputText,
+          outputPath: replaceFileExtToJs(options.filePath),
+        },
+      ];
+    },
Evidence
Within the same compiler object, getDistPathBySrcPath prepends dist/ while transpileFile
returns outputPath without it, so any logic expecting these to align will miscompute file
locations.

scopes/envs/envs/fallback-typescript-compiler.ts[34-57]

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 fallback compiler uses two different conventions:
- `getDistPathBySrcPath()` returns `dist/<relpath>.js`
- `transpileFile()` returns `outputPath` as `<relpath>.js` (no `dist/` prefix)
This makes `getDistPathBySrcPath()` disagree with actual outputs from `transpileFile()`.
### Fix Focus Areas
- scopes/envs/envs/fallback-typescript-compiler.ts[34-57]
### Suggested fix
Make `transpileFile().outputPath` and `getDistPathBySrcPath()` consistent by choosing one convention:
- Either emit `outputPath: 'dist/<relpath>.js'`, or
- Return `<relpath>.js` from `getDistPathBySrcPath()`.
Ensure the chosen convention matches how the rest of the compiler API is used in the codebase.

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


12. Legacy env ID shadowing 🐞 Bug ≡ Correctness
Description
EnvsMain.getEnvComponentByEnvId routes legacy core env IDs (e.g. teambit.react/react@<pinned>)
through Workspace.resolveComponentId(), but Workspace.resolveComponentId only bypasses
workspace-name collisions for core aspects. In a workspace that contains a component whose name
collides with a legacy env ID, the env string can resolve to the workspace component and Bit may
load the wrong env/aspect.
Code

scopes/envs/envs/environments.main.runtime.ts[R666-679]

+    // envs that used to be core aspects are configured without a version. fetching them without a
+    // version resolves to the latest, which may not exist locally - use the pinned version instead.
+    // when the env is a component of the host (e.g. in the bit repo itself), use the host version.
+    let envIdWithPotentialPinnedVersion = envId;
+    if (!envId.includes('@') && this.isLegacyCoreEnv(envId)) {
+      // prefer the host (workspace) component when it exists. getIdIfExist is a cheap in-memory
+      // lookup implemented by the workspace host; scope hosts don't implement it and always get
+      // the pinned version (listing all ids of a scope here would be too expensive).
+      const hostWithGetIdIfExist = host as { getIdIfExist?: (id: ComponentID) => ComponentID | undefined };
+      const fromHost = hostWithGetIdIfExist.getIdIfExist?.(ComponentID.fromString(envId));
+      envIdWithPotentialPinnedVersion = fromHost ? fromHost.toString() : resolveLegacyCoreEnvId(envId);
+    }
+    const newId = await host.resolveComponentId(envIdWithPotentialPinnedVersion);
const envComponent = await host.get(newId);
Evidence
The PR introduces a path where legacy core env IDs are pinned then resolved via
host.resolveComponentId(). Workspace resolution explicitly warns that (without a guard) a
core-aspect env ID like teambit.react/react-native can incorrectly resolve to a similarly named
workspace component, but the guard only applies when isCoreAspect() is true; removed legacy envs
are no longer core envs, so this protection does not apply to them.

scopes/envs/envs/environments.main.runtime.ts[260-267]
scopes/envs/envs/environments.main.runtime.ts[654-679]
scopes/envs/envs/legacy-core-envs.ts[11-20]
scopes/workspace/workspace/workspace.ts[2310-2318]

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

## Issue description
Legacy core env IDs are no longer core aspects, but they are still resolved via `host.resolveComponentId(...)`. `Workspace.resolveComponentId()` explicitly documents a name-collision case for core aspects and only short-circuits resolution when `aspectLoader.isCoreAspect(...)` is true. As a result, legacy env IDs can be incorrectly resolved to a similarly named workspace component.
### Issue Context
- Legacy core env IDs include `teambit.react/react`, `teambit.harmony/node`, etc., and are pinned to versions.
- `EnvsMain.getCoreEnvsIds()` now returns only the new default `empty-env`, so removed envs are not treated as core envs/aspects.
### How to fix
- Add a similar short-circuit in `Workspace.resolveComponentId()` for legacy core env IDs (ignore version), returning `ComponentID.fromString(id.toString())` before any bitmap/workspace-name based resolution.
- This keeps legacy env IDs unshadowable by workspace components, matching the intent of the existing core-aspect collision guard.
- Alternatively (or additionally), in `EnvsMain.getEnvComponentByEnvId()` avoid calling `host.resolveComponentId()` for legacy core envs when the input is already a fully qualified Bit ID (especially after pinning), and instead use `ComponentID.fromString(...)` directly.
### Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[654-679]
- scopes/workspace/workspace/workspace.ts[2310-2318]

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


13. Async push order nondeterminism 🐞 Bug ☼ Reliability
Description
WorkspaceAspectsLoader partitions legacy env seeders using Promise.all while pushing into shared
arrays after awaited operations, so the resulting graphSeeders order becomes dependent on async
completion timing rather than the original input order. This makes downstream aspect-graph seeding
timing-dependent and harder to reason about/debug (and can become flaky if any later logic depends
on seed order).
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R410-428]

+      await Promise.all(
+        graphSeeders.map(async (component) => {
+          const isNonWsLegacyEnv =
+            this.envs.isLegacyCoreEnv(component.id.toStringWithoutVersion()) &&
+            !(await this.workspace.hasId(component.id));
+          if (!isNonWsLegacyEnv) {
+            rest.push(component);
+            return;
+          }
+          const packagePath = await this.workspace.getComponentPackagePath(component);
+          if (await fs.pathExists(packagePath)) {
+            installedLegacyEnvs.push(component);
+            rest.push(component);
+          } else {
+            nonInstalledLegacyEnvs.push(component);
+          }
+        })
+      );
+      graphSeeders = rest;
Evidence
The code performs async checks (await this.workspace.hasId(...), await fs.pathExists(...)) and
then mutates shared arrays via push(), which inherently makes the insertion order depend on which
async branch resolves first.

scopes/workspace/workspace/workspace-aspects-loader.ts[399-429]

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

## Issue description
`WorkspaceAspectsLoader` builds `graphSeeders` by running `Promise.all(graphSeeders.map(async ...))` and mutating shared arrays (`rest`, `installedLegacyEnvs`, `nonInstalledLegacyEnvs`) via `push()` after `await` calls. Because the pushes happen after awaited I/O, the resulting arrays are filled in *completion order*, not the original `graphSeeders` order.
### Issue Context
This makes aspect-graph seeding timing-dependent. Even if ordering is not currently semantically required, it complicates debugging and can become flaky if any later stage implicitly relies on seed order.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[405-429]
### Suggested fix
Refactor the `Promise.all` block to be side-effect free:
- Return a structured result per input element (e.g. `{ index, component, includeInGraph, isInstalledLegacyEnv, isNonInstalledLegacyEnv }`).
- Use the order-preserving results of `Promise.all` (or `pMapSeries` / `for..of`) to rebuild `rest/installedLegacyEnvs/nonInstalledLegacyEnvs` deterministically (e.g. sort by `index` then filter).

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



Informational

14. Non-deterministic unset order 🐞 Bug ☼ Reliability
Description
WorkspaceAspectsManager.unsetAspectsFromComponents pushes updated component IDs into a shared array
from inside a Promise.all loop, making the returned order timing-dependent. This can cause flaky
tests or confusing CLI output ordering for bit aspect unset.
Code

scopes/workspace/workspace/workspace-aspects-manager.ts[R99-113]

+        const existAspect = component.state.aspects.get(aspectId.toStringWithoutVersion());
+        if (!existAspect) return;
+        await this.workspace.removeSpecificComponentConfig(component.id, existAspect.id.toString(), true);
+        updatedCompIds.push(component.id);
+      })
+    );
+    await this.workspace.bitMap.write(`aspect-unset (${aspectId})`);
+    return updatedCompIds;
+  }
+
+  /**
+   * returns all aspects info of a component, include the config and the data.
+   */
+  async getAspectsOfComponent(id: string | ComponentID): Promise<AspectList> {
+    if (typeof id === 'string') {
Evidence
The method uses Promise.all(components.map(async ...)) and mutates updatedCompIds within the
async callbacks, which makes the resulting ordering completion-dependent.

scopes/workspace/workspace/workspace-aspects-manager.ts[99-113]

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

## Issue description
`updatedCompIds.push(component.id)` is executed inside `Promise.all`, so ordering depends on async completion timing.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-manager.ts[99-113]
### Suggested fix
Return the updated component id from the async callback and build the final list via `compact(await Promise.all(...))`, or switch to `p-map-series` if you want to preserve input order.

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


Grey Divider

Qodo Logo

Comment thread scopes/workspace/workspace/workspace.ts Outdated
Comment thread scopes/envs/envs/fallback-typescript-compiler.ts
Comment thread scopes/workspace/workspace/workspace-aspects-loader.ts Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

qodo-free-for-open-source-projects Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Comment thread scopes/workspace/workspace/workspace-aspects-loader.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

Comment thread scopes/harmony/empty-env/empty-env.aspect.ts
Comment thread scopes/envs/envs/environments.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3f5c24e

…nv templates on demand

- register legacy core env ids as core extension names so their config entries stay name-only
  (versionless): prevents env-as-dependency edges that created circular TS project references
  in lane/tag builds
- require the aspect env eslint/prettier configs lazily (saves ~400 file reads per bit command)
- bit create: fall back to --env for template lookup, incl. templates registered on the
  generator slot by envs loaded from the global scope
- rewrite e2e node-env fixtures to compose on the core aspect env instead of @teambit/node
Comment thread scopes/harmony/aspect/aspect.env.ts Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0607c7d

- teambit.harmony/aspect and teambit.envs/env become regular envs with pinned
  legacy versions. components using them get the exact released behavior after
  bit install (the react-free aspect env rewrite is reverted)
- move the bit-aspect template and harmony starters to the generator aspect so
  'bit create bit-aspect' and 'bit new' work without loading the env
- bind manifest deps of legacy envs to pinned versions in scope context (models
  built when these envs were core don't list them as dependencies)
- load the full manifest graph when loading aspects from the global scope
- keep legacy core env ids versionless when configured via bit create/env set
Comment thread scopes/workspace/workspace/workspace.ts
Comment thread components/legacy/e2e-helper/e2e-env-helper.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

…ion when available

- fixes the ci snap failure: pinned-version copies of workspace components
  leaked into the load groups and into the snap list
- review fixes: index-based BFS queue in getDependentsIds, guard the
  typescript require in the fallback compiler, suppress legacy-env load
  failures only when the env package itself is missing, match both quote
  styles when detecting fixture env packages
Comment thread scopes/generator/generator/builtin-templates.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 94eddce

Comment thread scopes/compilation/compiler/compiler.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5422e3c

Comment thread scopes/envs/envs/environments.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

Comment thread scopes/envs/envs/environments.main.runtime.ts
Comment thread scopes/envs/envs/fallback-typescript-compiler.ts
Comment thread scopes/workspace/workspace/workspace-aspects-manager.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

Comment thread scopes/scope/scope/scope-aspects-loader.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

Comment thread scopes/dependencies/yarn/yarn.package-manager.ts
Comment thread scopes/envs/envs/environments.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 34e4544

Comment thread scopes/envs/envs/environments.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5b7b9cb

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4caa63f

Comment thread scopes/workspace/workspace/workspace-aspects-loader.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 11930cb

…ed ci-merge status

The fallback (return the configured env id when no aspect entry matches) is
correct for getEnvId reporting but too aggressive for calculateEnvId, which
feeds addNonLoadedEnvAsComponentIssues: it surfaced a NonLoadedEnv/ExternalEnvWithoutVersion
tag-blocker during 'bit ci merge' (env-set on a lane), failing status verification.
Comment on lines +127 to +144
const [inFlightIds, idsToLoad] = partition(notLoadedIds, (id) =>
this.workspace.inFlightAspectsLoads.has(aspectLoadInFlightKey(id))
);
if (inFlightIds.length) {
this.logger.debug(`${loggerPrefix} skipping aspects that are already loading: ${inFlightIds.join(', ')}`);
}
notLoadedIds = idsToLoad;
if (!notLoadedIds.length) {
span.setAttribute('alreadyLoaded', true);
return [];
}
const inFlightAdded = notLoadedIds.map(aspectLoadInFlightKey);
inFlightAdded.forEach((id) => this.workspace.inFlightAspectsLoads.add(id));
try {
return await this.loadAspectsAfterInFlightCheck(notLoadedIds, span, neededFor, mergedOpts, loggerPrefix);
} finally {
inFlightAdded.forEach((id) => this.workspace.inFlightAspectsLoads.delete(id));
}

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. In-flight key mismatch 🐞 Bug ☼ Reliability

WorkspaceAspectsLoader tracks in-flight loads using the pre-resolved aspect id string, but then
resolves ids to versioned ComponentIDs before loading; versionless and versioned requests for the
same aspect won’t match and can run duplicate concurrent loads. This undermines the intended
cycle/dedup guard and can re-run non-idempotent initialization work in parallel.
Agent Prompt
### Issue description
`WorkspaceAspectsLoader` uses `workspace.inFlightAspectsLoads` to dedupe/break re-entrant loads, but it checks/adds keys based on the *raw requested ids* (`notLoadedIds`). Later in the same flow it resolves those ids to canonical `ComponentID`s (often versioned) and loads by those resolved ids.

If the same aspect/env is requested concurrently once as `scope/name` and once as `scope/name@x.y.z`, the in-flight Set will treat them as different keys (for non-legacy aspects), allowing duplicate concurrent loads.

### Issue Context
The codebase explicitly supports env/aspect references without a version in some flows (e.g. envs-config / legacy ids), so versionless inputs are expected.

### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[121-203]
- scopes/envs/envs/legacy-core-envs.ts[57-66]

### Suggested fix
Canonicalize the in-flight key to match the canonical id used for loading:
- Resolve ids (or at least normalize) **before** `inFlightAspectsLoads.has/add/delete`.
- For versionless requested ids, consider using `id.split('@')[0]` as the in-flight key (or store *both* the versionless key and the resolved versioned key) so that later versioned requests map to the same in-flight entry.
- Keep legacy-core-env single-instance semantics intact (current `aspectLoadInFlightKey()` behavior).

ⓘ 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 6588503

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.

1 participant