Skip to content

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

Open
davidfirst wants to merge 8 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 8 commits into
masterfrom
remove-core-envs-from-manifest

Conversation

@davidfirst

@davidfirst davidfirst commented Jul 2, 2026

Copy link
Copy Markdown
Member

Removes the heavy env aspects (teambit.react/react, teambit.harmony/node, teambit.mdx/mdx, teambit.mdx/readme) from the core manifest to slim Bit. They now act like any other env: installed from the registry and configured with a version.

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 stay core, rewritten react-free. The aspect env is now standalone on top of the core typescript/babel/jest tooling (own tsconfig/jest/eslint/prettier configs, no react merge; template-bundler/preview dropped as they require the react stack). The env env composes from it as before. This keeps aspects and custom envs compiling/loading/typed out of the box - required for aspect development and the entire aspect/custom-env e2e surface.

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). This also means they never become dependency edges of their 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.

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).

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). The aspect env's eslint config chain is now required lazily (~400 fewer file reads per command). The e2e node-env fixtures compose on the core aspect env instead of @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 (4) 📘 Rule violations (2) 📜 Skill insights (0)

Grey Divider


Action required

1. AspectEnv.getLinter() uses ESLint 📘 Rule violation ⚙ Maintainability ⭐ New
Description
AspectEnv introduces an ESLint-backed linter (ESLintLinter) and imports eslint, which
reintroduces ESLint-based linting into the repo despite the standard being Oxlint + TypeScript
checks. This can create inconsistent linting behavior and increases maintenance/runtime dependency
on ESLint in core tooling.
Code

scopes/harmony/aspect/aspect.env.ts[R149-165]

+  getLinter(context: LinterContext, transformers: EslintConfigTransformer[] = []): Linter {
+    const tsconfigPath = require.resolve('./typescript/tsconfig.json');
+    const mergedOptions = {
+      // @ts-ignore - this is a bug in the @types/eslint types
+      overrideConfig: getEslintConfig() as ESLintLib.Options,
+      extensions: context.extensionFormats,
+      useEslintrc: false,
+      cwd: __dirname,
+      fix: !!context.fix,
+      fixTypes: context.fixTypes as ESLintLib.Options['fixTypes'],
+    } as ESLintOptions;
+    const configMutator = new EslintConfigMutator(mergedOptions);
+    const transformerContext: EslintConfigTransformContext = { fix: !!context.fix };
+    configMutator.setTsConfig(tsconfigPath);
+    const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext);
+    return ESLintLinter.create(afterMutation.raw, { logger: this.logger });
+  }
Evidence
PR Compliance ID 3 forbids adding ESLint-based linting back into the repository. The PR adds an
eslint import and a new getLinter() implementation that constructs ESLint options and returns an
ESLintLinter, which is direct ESLint usage.

CLAUDE.md: Do Not Introduce ESLint-Based Linting (Repo Uses Oxlint + TypeScript Type Checking)
scopes/harmony/aspect/aspect.env.ts[15-29]
scopes/harmony/aspect/aspect.env.ts[149-165]

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

## Issue description
`AspectEnv` now provides linting via `ESLintLinter` and imports `eslint`, which conflicts with the repo compliance requirement to avoid ESLint-based linting and rely on Oxlint + TypeScript type checking.

## Issue Context
The new `getLinter()` implementation builds an ESLint options object (including `overrideConfig`) and returns `ESLintLinter.create(...)`, making ESLint part of the default tooling surface for this environment.

## Fix Focus Areas
- scopes/harmony/aspect/aspect.env.ts[15-29]
- scopes/harmony/aspect/aspect.env.ts[149-165]

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


2. Legacy env id collision ✓ Resolved 🐞 Bug ≡ Correctness
Description
WorkspaceComponentLoader now resolves DEFAULT_ENV/legacy-core env ids via
workspace.resolveComponentId(), which may incorrectly resolve to an unrelated workspace component
with the same name because the bitmap lookup can strip the provided scope and match by name only.
This can cause the wrong env/aspect to load or be installed, breaking env resolution/tooling for the
workspace.
Code

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[R274-288]

+    // components with no env configured use the default env. as the default env is not a core
+    // aspect anymore, add it to the load groups so it gets loaded like any other env.
+    const hasComponentsWithoutEnv = allIds.some((id) => {
+      const fromCache = this.componentsExtensionsCache.get(id.toString());
+      return fromCache && !fromCache.envId;
+    });
+    if (hasComponentsWithoutEnv) {
+      nameOnlyLegacyEnvExtensions.add(DEFAULT_ENV);
+    }
+    await Promise.all(
+      Array.from(nameOnlyLegacyEnvExtensions).map(async (envIdStr) => {
+        if (allExtIds.has(envIdStr)) return;
+        const resolved = await this.workspace.resolveComponentId(envIdStr);
+        allExtIds.set(envIdStr, resolved);
+      })
Evidence
The loader now forces DEFAULT_ENV/legacy env ids through resolveComponentId(). That resolver uses
bitmap lookups with searchWithoutScopeInProvidedId=true, and BitMap’s implementation explicitly
strips the provided scope and matches by name, enabling collisions. Since the default env is "not a
core aspect anymore", the existing core-aspect collision guard does not apply.

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[274-289]
scopes/workspace/workspace/workspace.ts[2299-2316]
components/legacy/bit-map/bit-map.ts[588-616]
scopes/envs/envs/environments.main.runtime.ts[230-239]

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 (e.g. `teambit.harmony/node`) are no longer core aspects, but are still intended to behave as “reserved” ids. The new logic in `WorkspaceComponentLoader.buildLoadGroups()` calls `workspace.resolveComponentId(envIdStr)` for `DEFAULT_ENV` and other legacy env ids, and `Workspace.resolveComponentId()` can resolve them via bitmap lookups that **strip the provided scope and match by name only**.
If the workspace has a component named `node`/`react`/etc., this can resolve `teambit.harmony/node` to that local component, leading to loading/installing the wrong env.
### Issue Context
`Workspace.resolveComponentId()` already has a special-case to avoid this exact class of bug for **core aspects**, but legacy-core envs are no longer core aspects so the guard doesn’t apply.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[274-289]
- scopes/workspace/workspace/workspace.ts[2299-2316]
### Suggested fix approach
Implement one of these (prefer the central fix):
1) **Central fix (recommended):** In `Workspace.resolveComponentId()`, add a guard similar to the core-aspect guard for legacy-core env ids (match `id.split('@')[0]` against `envs.isLegacyCoreEnv(...)`). When it’s a legacy-core env, avoid any bitmap resolution mode that strips the provided scope (i.e. do not call `getParsedIdIfExist(..., /*searchWithoutScopeInProvidedId=*/true)` for these ids). Keep the ability to resolve the env component if it genuinely exists in the workspace/scope by using only “exact”/“strip scope from bitmap entry” matching.
2) **Local fix (alternative):** In `buildLoadGroups()`, when iterating `nameOnlyLegacyEnvExtensions`, avoid `workspace.resolveComponentId(envIdStr)` for legacy-core env ids. Instead, parse as `ComponentID.fromString(envIdStr)` and then (optionally) map to an exact workspace id only if it matches by full id (not by name-only).

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


3. Wrong parent chosen 🐞 Bug ≡ Correctness
Description
resolveInstalledAspectRecursively() follows only the first inbound edge (single parent) and memoizes
a null result on failure, so aspects with multiple parents may be incorrectly marked unresolvable
even when another parent chain could resolve them from node_modules.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R815-831]

+    // guard against circular dependencies in the graph
+    if (visiting.has(aspectStringId)) return undefined;
+    visiting.add(aspectStringId);
if (rootIds.includes(aspectStringId)) {
const localPath = await this.workspace.getComponentPackagePath(aspectComponent);
this.resolvedInstalledAspects.set(aspectStringId, localPath);
return localPath;
}
-    const parent = graph.predecessors(aspectStringId)[0];
+    // use inEdges to get the immediate parent. don't use graph.predecessors() as it returns all
+    // the recursive predecessors, which may throw "Maximum call stack size exceeded" on big graphs
+    const parentEdge = graph.inEdges(aspectStringId)[0];
+    const parent = parentEdge ? graph.node(parentEdge.sourceId) : undefined;
if (!parent) return undefined;
-    const parentPath = await this.resolveInstalledAspectRecursively(parent.attr, rootIds, graph);
+    const parentPath = await this.resolveInstalledAspectRecursively(parent.attr, rootIds, graph, opts, visiting);
if (!parentPath) {
this.resolvedInstalledAspects.set(aspectStringId, null);
return undefined;
Evidence
The new implementation explicitly selects only the first inEdges() entry as the parent and caches
null on failure, which prevents trying any alternative parent edges for nodes that have more than
one inbound edge.

scopes/workspace/workspace/workspace-aspects-loader.ts[803-850]

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

## Issue description
`resolveInstalledAspectRecursively()` picks `graph.inEdges(id)[0]` as the only parent to walk, and if that path fails it caches `null` in `resolvedInstalledAspects`. In graphs where a node has multiple parents (common in dependency graphs), this can incorrectly prevent resolution via a different parent chain.
### Issue Context
This function is used to resolve aspects from installed packages, and caching failures globally per-aspect makes the behavior sensitive to graph edge ordering.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[803-850]
### Suggested fix
1. Replace the single-parent selection with a loop over *all* `graph.inEdges(aspectStringId)`.
2. For each candidate parent edge:
- Resolve `parentPath` recursively (consider passing a cloned `visiting` set per branch).
- Attempt `resolveFrom(parentPath, packageName)`.
- On first success: cache the resolved path and return it.
3. Only cache a terminal failure (`null`) after *all* parents were tried and failed.
4. Avoid caching `null` from a single parent attempt, because another parent may succeed.

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



Remediation recommended

4. empty-env missing docs files 📘 Rule violation ⚙ Maintainability
Description
A new aspect teambit.harmony/empty-env was added without the standard documentation/composition
artifacts (e.g. .docs.mdx, .composition.tsx) expected for discoverability and maintainability.
This diverges from the repository’s standard aspect structure expectations.
Code

scopes/harmony/empty-env/empty-env.aspect.ts[R1-7]

+import { Aspect } from '@teambit/harmony';
+
+export const EmptyEnvAspect = Aspect.create({
+  id: 'teambit.harmony/empty-env',
+});
+
+export default EmptyEnvAspect;
Evidence
PR Compliance ID 3 requires new/restructured aspects to follow the standard artifact structure
(including docs/compositions where applicable). This PR adds the EmptyEnvAspect and its main
runtime, but no .docs.mdx/composition artifacts are added alongside it in the new aspect
directory.

CLAUDE.md: Aspects Must Follow the Standard File/Artifact Structure: CLAUDE.md: Aspects Must Follow the Standard File/Artifact Structure: CLAUDE.md: Aspects Must Follow the Standard File/Artifact Structure: CLAUDE.md: Aspects Must Follow the Standard File/Artifact Structure
scopes/harmony/empty-env/empty-env.aspect.ts[1-7]
scopes/harmony/empty-env/empty-env.main.runtime.ts[1-44]
scopes/harmony/empty-env/index.ts[1-3]

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

## Issue description
A new aspect (`teambit.harmony/empty-env`) was introduced, but it doesn’t include standard aspect artifacts like documentation (`.docs.mdx`) and (when applicable) compositions (`.composition.tsx`). This makes the aspect harder to discover and maintain.
## Issue Context
The PR introduces the new `EmptyEnvAspect` and its main runtime, but no accompanying aspect docs/compositions were added.
## Fix Focus Areas
- scopes/harmony/empty-env/empty-env.aspect.ts[1-7]
- scopes/harmony/empty-env/empty-env.main.runtime.ts[1-44]
- scopes/harmony/empty-env/index.ts[1-3]

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


5. Fallback env reports node ✓ Resolved 🐞 Bug ≡ Correctness
Description
When a legacy core env isn’t loaded yet, EnvsMain falls back to a minimal env whose descriptor is
hard-coded to { type: 'node' } and name: 'node', so non-node legacy envs (react/mdx/html) can
get persisted/displayed as “node”. This env descriptor is persisted into extension data during scope
component recalculation, so the misleading metadata can survive beyond the transient “env not
loaded” state.
Code

scopes/envs/envs/environments.main.runtime.ts[R244-250]

+  private getFallbackDefaultEnv(): Environment {
+    if (!this.fallbackDefaultEnv) {
+      this.fallbackDefaultEnv = {
+        name: 'node',
+        getCompiler: () => getFallbackTypescriptCompiler(),
+        __getDescriptor: async () => ({ type: 'node' }),
+      };
Evidence
EnvsMain.getEnv() explicitly returns the fallback env for legacy core env IDs without a version; the
fallback env hard-codes name/type as node; env descriptors use __getDescriptor().type and
envDef.name; and scope recalculation persists envs.calcDescriptor() into extension data, so the
misleading descriptor can be stored in the model.

scopes/envs/envs/environments.main.runtime.ts[237-253]
scopes/envs/envs/environments.main.runtime.ts[553-567]
scopes/envs/envs/environments.main.runtime.ts[696-710]
scopes/envs/envs/legacy-core-envs.ts[11-18]
scopes/scope/scope/scope.main.runtime.ts[329-357]

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 envs that are not installed/loaded fall back to `getFallbackDefaultEnv()`, but that fallback env always reports `name: 'node'` and `__getDescriptor(): { type: 'node' }`. This causes components whose configured env is *not* node (e.g. react/mdx/html legacy core envs) to get an incorrect env descriptor (type/name) that can be persisted into model extension data during component recalculation.
### Issue Context
- The fallback is returned from `EnvsMain.getEnv()` specifically for legacy core env IDs without a version.
- `EnvsMain.calcDescriptor()` is used during scope-side recalculation and then persisted via `upsertExtensionData`.
### Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[237-253]
- scopes/envs/envs/environments.main.runtime.ts[553-564]
- scopes/envs/envs/environments.main.runtime.ts[696-710]
- scopes/scope/scope/scope.main.runtime.ts[329-357]
### Suggested fix
Change the fallback env descriptor/name so it cannot misrepresent itself as a specific env (node). Options:
1. Make the fallback env report a neutral identity (e.g. `name: 'fallback'`, `type: 'fallback'`).
2. Preferably: make the fallback env factory accept the requested env id (without version) and return an env whose `name/type` reflect that env id (or at least `type: 'missing-env'`), while still providing the minimal TS transpiler.
Also ensure the persisted descriptor remains accurate enough for user-facing commands (show/envs/status) and doesn’t mislabel legacy react/mdx/html envs as node.

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


6. In-flight ignores version 🐞 Bug ☼ Reliability
Description
WorkspaceAspectsLoader.loadAspectsWithSpan() de-duplicates in-flight loads using id-without-version
(id.split('@')[0]), so a request for foo@2.0.0 can be dropped while foo@1.0.0 is still loading and
the caller returns without ensuring the requested version is loaded. This can leave Harmony missing
required aspects or using an unintended version, causing non-deterministic runtime failures during
aspect/env resolution.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R124-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.
+    notLoadedIds = notLoadedIds.filter((id) => !this.workspace.inFlightAspectsLoads.has(id.split('@')[0]));
if (!notLoadedIds.length) {
 span.setAttribute('alreadyLoaded', true);
 return [];
}
+    const inFlightAdded = notLoadedIds.map((id) => id.split('@')[0]);
+    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 filters notLoadedIds using inFlightAspectsLoads.has(id.split('@')[0]) and also
inserts into the set using id.split('@')[0], so all versions of the same id share the same
in-flight key and may be suppressed. The Workspace field is documented as storing ids without
version, and the loader comments indicate versioned ids are expected in this code path.

scopes/workspace/workspace/workspace-aspects-loader.ts[120-137]
scopes/workspace/workspace/workspace-aspects-loader.ts[149-154]
scopes/workspace/workspace/workspace.ts[210-214]

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()` uses `workspace.inFlightAspectsLoads` keyed by `id.split('@')[0]` (id-without-version). This conflates different versions of the same aspect/env and can cause a request for a different version to be filtered out and never awaited/loaded by the current call.
## Issue Context
- The in-flight set is explicitly documented as storing aspect-ids **without version**.
- The loader itself acknowledges that ids may be requested **with** versions.
## Fix Focus Areas
- Make the circular-load guard version-aware for non-legacy aspects (e.g., key by full id, or use a per-call stack keyed by full id).
- If you still need “ignore version” behavior, scope it narrowly to legacy-core envs (or other explicitly single-instance cases), not globally.
- Consider replacing the Set with a `Map<fullId, Promise>` (or similar) so concurrent requests can *join/await* the in-flight load rather than silently skipping.
### Target code
- scopes/workspace/workspace/workspace-aspects-loader.ts[124-137]
- scopes/workspace/workspace/workspace-aspects-loader.ts[150-154]
- scopes/workspace/workspace/workspace.ts[210-214]

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


View more (2)
7. Quadratic queue traversal 🐞 Bug ➹ Performance
Description
Workspace.getDependentsIds() now traverses iteratively but dequeues via Array.shift(), which is O(n)
per dequeue and can make the traversal O(n^2) on large graphs. This can significantly slow
dependents resolution on large/deep workspaces (the exact scenario the change targets).
Code

scopes/workspace/workspace/workspace.ts[R740-742]

+    while (queue.length) {
+      const current = queue.shift() as string;
+      graph.inEdges(current).forEach((edge) => {
Evidence
The implementation explicitly uses queue.shift() inside the traversal loop, which is the source of
the avoidable quadratic behavior for large queues.

scopes/workspace/workspace/workspace.ts[733-754]

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

## Issue description
`Workspace.getDependentsIds()` uses `queue.shift()` in a loop. In JS, `shift()` is O(n) because it re-indexes the array, so the traversal can degrade to O(n^2) on large graphs.
## Issue Context
This code path was explicitly changed to handle large/deep graphs without recursion overflow, so it should also avoid avoidable algorithmic slowdowns.
## Fix Focus Areas
- scopes/workspace/workspace/workspace.ts[733-754]
### Suggested fix
Use an index-based queue (or a deque implementation):
- `const queue = ids.map(...); let idx = 0; while (idx < queue.length) { const current = queue[idx++]; ... }`
This preserves FIFO semantics without O(n) dequeues.

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


8. Unguarded TypeScript require 🐞 Bug ☼ Reliability
Description
getFallbackTypescriptCompiler() calls require('typescript') without handling MODULE_NOT_FOUND, so
the fallback env can crash at runtime if TypeScript is not available. This is especially risky
because the fallback compiler is intended to be used specifically before "bit install" completes.
Code

scopes/envs/envs/fallback-typescript-compiler.ts[R8-18]

+export function getFallbackTypescriptCompiler() {
+  // eslint-disable-next-line global-require
+  const ts = require('typescript');
+  const supportedExtensions = ['.ts', '.tsx', '.jsx'];
+  const compilerOptions = {
+    module: ts.ModuleKind.CommonJS,
+    target: ts.ScriptTarget.ES2019,
+    jsx: ts.JsxEmit.React,
+    esModuleInterop: true,
+    sourceMap: false,
+  };
Evidence
The fallback compiler unconditionally requires 'typescript' at runtime, and the repository root
dependencies do not list TypeScript, so the fallback path can fail depending on installation state.

scopes/envs/envs/fallback-typescript-compiler.ts[8-18]
package.json[59-64]

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 does `require('typescript')` with no try/catch. If `typescript` isn't resolvable in the current runtime environment, this throws immediately and defeats the purpose of having a fallback env to keep basic flows working pre-install.
## Issue Context
The repo root `package.json` does not declare `typescript`, so availability depends on other installation mechanics; the fallback path should be defensive.
## Fix Focus Areas
- scopes/envs/envs/fallback-typescript-compiler.ts[8-18]
### Suggested fix
Wrap the require in try/catch and throw a clear, actionable error (or return a no-op compiler that surfaces a recoverable issue), e.g.:
- `let ts; try { ts = require('typescript'); } catch (e) { throw new Error('Fallback compiler requires the typescript package. Please run "bit install".'); }`
Optionally, also add a lightweight log/telemetry hook to make this failure diagnosable.

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


Grey Divider

Previous review results

Review updated until commit 0607c7d

Results up to commit 3f5c24e


🐞 Bugs (5) 📘 Rule violations (1) 📜 Skill insights (0)


Action required
1. Legacy env id collision ✓ Resolved 🐞 Bug ≡ Correctness
Description
WorkspaceComponentLoader now resolves DEFAULT_ENV/legacy-core env ids via
workspace.resolveComponentId(), which may incorrectly resolve to an unrelated workspace component
with the same name because the bitmap lookup can strip the provided scope and match by name only.
This can cause the wrong env/aspect to load or be installed, breaking env resolution/tooling for the
workspace.
Code

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[R274-288]

+    // components with no env configured use the default env. as the default env is not a core
+    // aspect anymore, add it to the load groups so it gets loaded like any other env.
+    const hasComponentsWithoutEnv = allIds.some((id) => {
+      const fromCache = this.componentsExtensionsCache.get(id.toString());
+      return fromCache && !fromCache.envId;
+    });
+    if (hasComponentsWithoutEnv) {
+      nameOnlyLegacyEnvExtensions.add(DEFAULT_ENV);
+    }
+    await Promise.all(
+      Array.from(nameOnlyLegacyEnvExtensions).map(async (envIdStr) => {
+        if (allExtIds.has(envIdStr)) return;
+        const resolved = await this.workspace.resolveComponentId(envIdStr);
+        allExtIds.set(envIdStr, resolved);
+      })
Evidence
The loader now forces DEFAULT_ENV/legacy env ids through resolveComponentId(). That resolver uses
bitmap lookups with searchWithoutScopeInProvidedId=true, and BitMap’s implementation explicitly
strips the provided scope and matches by name, enabling collisions. Since the default env is "not a
core aspect anymore", the existing core-aspect collision guard does not apply.

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[274-289]
scopes/workspace/workspace/workspace.ts[2299-2316]
components/legacy/bit-map/bit-map.ts[588-616]
scopes/envs/envs/environments.main.runtime.ts[230-239]

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 (e.g. `teambit.harmony/node`) are no longer core aspects, but are still intended to behave as “reserved” ids. The new logic in `WorkspaceComponentLoader.buildLoadGroups()` calls `workspace.resolveComponentId(envIdStr)` for `DEFAULT_ENV` and other legacy env ids, and `Workspace.resolveComponentId()` can resolve them via bitmap lookups that **strip the provided scope and match by name only**.
If the workspace has a component named `node`/`react`/etc., this can resolve `teambit.harmony/node` to that local component, leading to loading/installing the wrong env.
### Issue Context
`Workspace.resolveComponentId()` already has a special-case to avoid this exact class of bug for **core aspects**, but legacy-core envs are no longer core aspects so the guard doesn’t apply.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[274-289]
- scopes/workspace/workspace/workspace.ts[2299-2316]
### Suggested fix approach
Implement one of these (prefer the central fix):
1) **Central fix (recommended):** In `Workspace.resolveComponentId()`, add a guard similar to the core-aspect guard for legacy-core env ids (match `id.split('@')[0]` against `envs.isLegacyCoreEnv(...)`). When it’s a legacy-core env, avoid any bitmap resolution mode that strips the provided scope (i.e. do not call `getParsedIdIfExist(..., /*searchWithoutScopeInProvidedId=*/true)` for these ids). Keep the ability to resolve the env component if it genuinely exists in the workspace/scope by using only “exact”/“strip scope from bitmap entry” matching.
2) **Local fix (alternative):** In `buildLoadGroups()`, when iterating `nameOnlyLegacyEnvExtensions`, avoid `workspace.resolveComponentId(envIdStr)` for legacy-core env ids. Instead, parse as `ComponentID.fromString(envIdStr)` and then (optionally) map to an exact workspace id only if it matches by full id (not by name-only).

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


2. Wrong parent chosen 🐞 Bug ≡ Correctness
Description
resolveInstalledAspectRecursively() follows only the first inbound edge (single parent) and memoizes
a null result on failure, so aspects with multiple parents may be incorrectly marked unresolvable
even when another parent chain could resolve them from node_modules.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R815-831]

+    // guard against circular dependencies in the graph
+    if (visiting.has(aspectStringId)) return undefined;
+    visiting.add(aspectStringId);
if (rootIds.includes(aspectStringId)) {
  const localPath = await this.workspace.getComponentPackagePath(aspectComponent);
  this.resolvedInstalledAspects.set(aspectStringId, localPath);
  return localPath;
}
-    const parent = graph.predecessors(aspectStringId)[0];
+    // use inEdges to get the immediate parent. don't use graph.predecessors() as it returns all
+    // the recursive predecessors, which may throw "Maximum call stack size exceeded" on big graphs
+    const parentEdge = graph.inEdges(aspectStringId)[0];
+    const parent = parentEdge ? graph.node(parentEdge.sourceId) : undefined;
if (!parent) return undefined;
-    const parentPath = await this.resolveInstalledAspectRecursively(parent.attr, rootIds, graph);
+    const parentPath = await this.resolveInstalledAspectRecursively(parent.attr, rootIds, graph, opts, visiting);
if (!parentPath) {
  this.resolvedInstalledAspects.set(aspectStringId, null);
  return undefined;
Evidence
The new implementation explicitly selects only the first inEdges() entry as the parent and caches
null on failure, which prevents trying any alternative parent edges for nodes that have more than
one inbound edge.

scopes/workspace/workspace/workspace-aspects-loader.ts[803-850]

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

## Issue description
`resolveInstalledAspectRecursively()` picks `graph.inEdges(id)[0]` as the only parent to walk, and if that path fails it caches `null` in `resolvedInstalledAspects`. In graphs where a node has multiple parents (common in dependency graphs), this can incorrectly prevent resolution via a different parent chain.
### Issue Context
This function is used to resolve aspects from installed packages, and caching failures globally per-aspect makes the behavior sensitive to graph edge ordering.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[803-850]
### Suggested fix
1. Replace the single-parent selection with a loop over *all* `graph.inEdges(aspectStringId)`.
2. For each candidate parent edge:
- Resolve `parentPath` recursively (consider passing a cloned `visiting` set per branch).
- Attempt `resolveFrom(parentPath, packageName)`.
- On first success: cache the resolved path and return it.
3. Only cache a terminal failure (`null`) after *all* parents were tried and failed.
4. Avoid caching `null` from a single parent attempt, because another parent may succeed.

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



Remediation recommended
3. empty-env missing docs files 📘 Rule violation ⚙ Maintainability ⭐ New
Description
A new aspect teambit.harmony/empty-env was added without the standard documentation/composition
artifacts (e.g. .docs.mdx, .composition.tsx) expected for discoverability and maintainability.
This diverges from the repository’s standard aspect structure expectations.
Code

scopes/harmony/empty-env/empty-env.aspect.ts[R1-7]

+import { Aspect } from '@teambit/harmony';
+
+export const EmptyEnvAspect = Aspect.create({
+  id: 'teambit.harmony/empty-env',
+});
+
+export default EmptyEnvAspect;
Evidence
PR Compliance ID 3 requires new/restructured aspects to follow the standard artifact structure
(including docs/compositions where applicable). This PR adds the EmptyEnvAspect and its main
runtime, but no .docs.mdx/composition artifacts are added alongside it in the new aspect
directory.

CLAUDE.md: Aspects Must Follow the Standard File/Artifact Structure
scopes/harmony/empty-env/empty-env.aspect.ts[1-7]
scopes/harmony/empty-env/empty-env.main.runtime.ts[1-44]
scopes/harmony/empty-env/index.ts[1-3]

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

## Issue description
A new aspect (`teambit.harmony/empty-env`) was introduced, but it doesn’t include standard aspect artifacts like documentation (`.docs.mdx`) and (when applicable) compositions (`.composition.tsx`). This makes the aspect harder to discover and maintain.

## Issue Context
The PR introduces the new `EmptyEnvAspect` and its main runtime, but no accompanying aspect docs/compositions were added.

## Fix Focus Areas
- scopes/harmony/empty-env/empty-env.aspect.ts[1-7]
- scopes/harmony/empty-env/empty-env.main.runtime.ts[1-44]
- scopes/harmony/empty-env/index.ts[1-3]

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


4. Fallback env reports node 🐞 Bug ≡ Correctness ⭐ New
Description
When a legacy core env isn’t loaded yet, EnvsMain falls back to a minimal env whose descriptor is
hard-coded to { type: 'node' } and name: 'node', so non-node legacy envs (react/mdx/html) can
get persisted/displayed as “node”. This env descriptor is persisted into extension data during scope
component recalculation, so the misleading metadata can survive beyond the transient “env not
loaded” state.
Code

scopes/envs/envs/environments.main.runtime.ts[R244-250]

+  private getFallbackDefaultEnv(): Environment {
+    if (!this.fallbackDefaultEnv) {
+      this.fallbackDefaultEnv = {
+        name: 'node',
+        getCompiler: () => getFallbackTypescriptCompiler(),
+        __getDescriptor: async () => ({ type: 'node' }),
+      };
Evidence
EnvsMain.getEnv() explicitly returns the fallback env for legacy core env IDs without a version; the
fallback env hard-codes name/type as node; env descriptors use __getDescriptor().type and
envDef.name; and scope recalculation persists envs.calcDescriptor() into extension data, so the
misleading descriptor can be stored in the model.

scopes/envs/envs/environments.main.runtime.ts[237-253]
scopes/envs/envs/environments.main.runtime.ts[553-567]
scopes/envs/envs/environments.main.runtime.ts[696-710]
scopes/envs/envs/legacy-core-envs.ts[11-18]
scopes/scope/scope/scope.main.runtime.ts[329-357]

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 envs that are not installed/loaded fall back to `getFallbackDefaultEnv()`, but that fallback env always reports `name: 'node'` and `__getDescriptor(): { type: 'node' }`. This causes components whose configured env is *not* node (e.g. react/mdx/html legacy core envs) to get an incorrect env descriptor (type/name) that can be persisted into model extension data during component recalculation.

### Issue Context
- The fallback is returned from `EnvsMain.getEnv()` specifically for legacy core env IDs without a version.
- `EnvsMain.calcDescriptor()` is used during scope-side recalculation and then persisted via `upsertExtensionData`.

### Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[237-253]
- scopes/envs/envs/environments.main.runtime.ts[553-564]
- scopes/envs/envs/environments.main.runtime.ts[696-710]
- scopes/scope/scope/scope.main.runtime.ts[329-357]

### Suggested fix
Change the fallback env descriptor/name so it cannot misrepresent itself as a specific env (node). Options:
1. Make the fallback env report a neutral identity (e.g. `name: 'fallback'`, `type: 'fallback'`).
2. Preferably: make the fallback env factory accept the requested env id (without version) and return an env whose `name/type` reflect that env id (or at least `type: 'missing-env'`), while still providing the minimal TS transpiler.

Also ensure the persisted descriptor remains accurate enough for user-facing commands (show/envs/status) and doesn’t mislabel legacy react/mdx/html envs as node.

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


5. In-flight ignores version 🐞 Bug ☼ Reliability
Description
WorkspaceAspectsLoader.loadAspectsWithSpan() de-duplicates in-flight loads using id-without-version
(id.split('@')[0]), so a request for foo@2.0.0 can be dropped while foo@1.0.0 is still loading and
the caller returns without ensuring the requested version is loaded. This can leave Harmony missing
required aspects or using an unintended version, causing non-deterministic runtime failures during
aspect/env resolution.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R124-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.
+    notLoadedIds = notLoadedIds.filter((id) => !this.workspace.inFlightAspectsLoads.has(id.split('@')[0]));
 if (!notLoadedIds.length) {
   span.setAttribute('alreadyLoaded', true);
   return [];
 }
+    const inFlightAdded = notLoadedIds.map((id) => id.split('@')[0]);
+    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 filters notLoadedIds using inFlightAspectsLoads.has(id.split('@')[0]) and also
inserts into the set using id.split('@')[0], so all versions of the same id share the same
in-flight key and may be suppressed. The Workspace field is documented as storing ids without
version, and the loader comments indicate versioned ids are expected in this code path.

scopes/workspace/workspace/workspace-aspects-loader.ts[120-137]
scopes/workspace/workspace/workspace-aspects-loader.ts[149-154]
scopes/workspace/workspace/workspace.ts[210-214]

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()` uses `workspace.inFlightAspectsLoads` keyed by `id.split('@')[0]` (id-without-version). This conflates different versions of the same aspect/env and can cause a request for a different version to be filtered out and never awaited/loaded by the current call.
## Issue Context
- The in-flight set is explicitly documented as storing aspect-ids **without version**.
- The loader itself acknowledges that ids may be requested **with** versions.
## Fix Focus Areas
- Make the circular-load guard version-aware for non-legacy aspects (e.g., key by full id, or use a per-call stack keyed by full id).
- If you still need “ignore version” behavior, scope it narrowly to legacy-core envs (or other explicitly single-instance cases), not globally.
- Consider replacing the Set with a `Map<fullId, Promise>` (or similar) so concurrent requests can *join/await* the in-flight load rather than silently skipping.
### Target code
- scopes/workspace/workspace/workspace-aspects-loader.ts[124-137]
- scopes/workspace/workspace/workspace-aspects-loader.ts[150-154]
- scopes/workspace/workspace/workspace.ts[210-214]

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


View more (2)
6. Quadratic queue traversal 🐞 Bug ➹ Performance
Description
Workspace.getDependentsIds() now traverses iteratively but dequeues via Array.shift(), which is O(n)
per dequeue and can make the traversal O(n^2) on large graphs. This can significantly slow
dependents resolution on large/deep workspaces (the exact scenario the change targets).
Code

scopes/workspace/workspace/workspace.ts[R740-742]

+    while (queue.length) {
+      const current = queue.shift() as string;
+      graph.inEdges(current).forEach((edge) => {
Evidence
The implementation explicitly uses queue.shift() inside the traversal loop, which is the source of
the avoidable quadratic behavior for large queues.

scopes/workspace/workspace/workspace.ts[733-754]

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

## Issue description
`Workspace.getDependentsIds()` uses `queue.shift()` in a loop. In JS, `shift()` is O(n) because it re-indexes the array, so the traversal can degrade to O(n^2) on large graphs.
## Issue Context
This code path was explicitly changed to handle large/deep graphs without recursion overflow, so it should also avoid avoidable algorithmic slowdowns.
## Fix Focus Areas
- scopes/workspace/workspace/workspace.ts[733-754]
### Suggested fix
Use an index-based queue (or a deque implementation):
- `const queue = ids.map(...); let idx = 0; while (idx < queue.length) { const current = queue[idx++]; ... }`
This preserves FIFO semantics without O(n) dequeues.

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


7. Unguarded TypeScript require 🐞 Bug ☼ Reliability
Description
getFallbackTypescriptCompiler() calls require('typescript') without handling MODULE_NOT_FOUND, so
the fallback env can crash at runtime if TypeScript is not available. This is especially risky
because the fallback compiler is intended to be used specifically before "bit install" completes.
Code

scopes/envs/envs/fallback-typescript-compiler.ts[R8-18]

+export function getFallbackTypescriptCompiler() {
+  // eslint-disable-next-line global-require
+  const ts = require('typescript');
+  const supportedExtensions = ['.ts', '.tsx', '.jsx'];
+  const compilerOptions = {
+    module: ts.ModuleKind.CommonJS,
+    target: ts.ScriptTarget.ES2019,
+    jsx: ts.JsxEmit.React,
+    esModuleInterop: true,
+    sourceMap: false,
+  };
Evidence
The fallback compiler unconditionally requires 'typescript' at runtime, and the repository root
dependencies do not list TypeScript, so the fallback path can fail depending on installation state.

scopes/envs/envs/fallback-typescript-compiler.ts[8-18]
package.json[59-64]

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 does `require('typescript')` with no try/catch. If `typescript` isn't resolvable in the current runtime environment, this throws immediately and defeats the purpose of having a fallback env to keep basic flows working pre-install.
## Issue Context
The repo root `package.json` does not declare `typescript`, so availability depends on other installation mechanics; the fallback path should be defensive.
## Fix Focus Areas
- scopes/envs/envs/fallback-typescript-compiler.ts[8-18]
### Suggested fix
Wrap the require in try/catch and throw a clear, actionable error (or return a no-op compiler that surfaces a recoverable issue), e.g.:
- `let ts; try { ts = require('typescript'); } catch (e) { throw new Error('Fallback compiler requires the typescript package. Please run "bit install".'); }`
Optionally, also add a lightweight log/telemetry hook to make this failure diagnosable.

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


Qodo Logo

Comment on lines +740 to +742
while (queue.length) {
const current = queue.shift() as string;
graph.inEdges(current).forEach((edge) => {

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. Quadratic queue traversal 🐞 Bug ➹ Performance

Workspace.getDependentsIds() now traverses iteratively but dequeues via Array.shift(), which is O(n)
per dequeue and can make the traversal O(n^2) on large graphs. This can significantly slow
dependents resolution on large/deep workspaces (the exact scenario the change targets).
Agent Prompt
## Issue description
`Workspace.getDependentsIds()` uses `queue.shift()` in a loop. In JS, `shift()` is O(n) because it re-indexes the array, so the traversal can degrade to O(n^2) on large graphs.

## Issue Context
This code path was explicitly changed to handle large/deep graphs without recursion overflow, so it should also avoid avoidable algorithmic slowdowns.

## Fix Focus Areas
- scopes/workspace/workspace/workspace.ts[733-754]

### Suggested fix
Use an index-based queue (or a deque implementation):
- `const queue = ids.map(...); let idx = 0; while (idx < queue.length) { const current = queue[idx++]; ... }`
This preserves FIFO semantics without O(n) dequeues.

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

Comment on lines +8 to +18
export function getFallbackTypescriptCompiler() {
// eslint-disable-next-line global-require
const ts = require('typescript');
const supportedExtensions = ['.ts', '.tsx', '.jsx'];
const compilerOptions = {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2019,
jsx: ts.JsxEmit.React,
esModuleInterop: true,
sourceMap: false,
};

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. Unguarded typescript require 🐞 Bug ☼ Reliability

getFallbackTypescriptCompiler() calls require('typescript') without handling MODULE_NOT_FOUND, so
the fallback env can crash at runtime if TypeScript is not available. This is especially risky
because the fallback compiler is intended to be used specifically before "bit install" completes.
Agent Prompt
## Issue description
The fallback compiler does `require('typescript')` with no try/catch. If `typescript` isn't resolvable in the current runtime environment, this throws immediately and defeats the purpose of having a fallback env to keep basic flows working pre-install.

## Issue Context
The repo root `package.json` does not declare `typescript`, so availability depends on other installation mechanics; the fallback path should be defensive.

## Fix Focus Areas
- scopes/envs/envs/fallback-typescript-compiler.ts[8-18]

### Suggested fix
Wrap the require in try/catch and throw a clear, actionable error (or return a no-op compiler that surfaces a recoverable issue), e.g.:
- `let ts; try { ts = require('typescript'); } catch (e) { throw new Error('Fallback compiler requires the typescript package. Please run "bit install".'); }`
Optionally, also add a lightweight log/telemetry hook to make this failure diagnosable.

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

Comment on lines +815 to 831
// guard against circular dependencies in the graph
if (visiting.has(aspectStringId)) return undefined;
visiting.add(aspectStringId);
if (rootIds.includes(aspectStringId)) {
const localPath = await this.workspace.getComponentPackagePath(aspectComponent);
this.resolvedInstalledAspects.set(aspectStringId, localPath);
return localPath;
}
const parent = graph.predecessors(aspectStringId)[0];
// use inEdges to get the immediate parent. don't use graph.predecessors() as it returns all
// the recursive predecessors, which may throw "Maximum call stack size exceeded" on big graphs
const parentEdge = graph.inEdges(aspectStringId)[0];
const parent = parentEdge ? graph.node(parentEdge.sourceId) : undefined;
if (!parent) return undefined;
const parentPath = await this.resolveInstalledAspectRecursively(parent.attr, rootIds, graph);
const parentPath = await this.resolveInstalledAspectRecursively(parent.attr, rootIds, graph, opts, visiting);
if (!parentPath) {
this.resolvedInstalledAspects.set(aspectStringId, null);
return undefined;

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. Wrong parent chosen 🐞 Bug ≡ Correctness

resolveInstalledAspectRecursively() follows only the first inbound edge (single parent) and memoizes
a null result on failure, so aspects with multiple parents may be incorrectly marked unresolvable
even when another parent chain could resolve them from node_modules.
Agent Prompt
### Issue description
`resolveInstalledAspectRecursively()` picks `graph.inEdges(id)[0]` as the only parent to walk, and if that path fails it caches `null` in `resolvedInstalledAspects`. In graphs where a node has multiple parents (common in dependency graphs), this can incorrectly prevent resolution via a different parent chain.

### Issue Context
This function is used to resolve aspects from installed packages, and caching failures globally per-aspect makes the behavior sensitive to graph edge ordering.

### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[803-850]

### Suggested fix
1. Replace the single-parent selection with a loop over *all* `graph.inEdges(aspectStringId)`.
2. For each candidate parent edge:
   - Resolve `parentPath` recursively (consider passing a cloned `visiting` set per branch).
   - Attempt `resolveFrom(parentPath, packageName)`.
   - On first success: cache the resolved path and return it.
3. Only cache a terminal failure (`null`) after *all* parents were tried and failed.
4. Avoid caching `null` from a single parent attempt, because another parent may succeed.

ⓘ 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 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 on lines +124 to +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.
notLoadedIds = notLoadedIds.filter((id) => !this.workspace.inFlightAspectsLoads.has(id.split('@')[0]));
if (!notLoadedIds.length) {
span.setAttribute('alreadyLoaded', true);
return [];
}
const inFlightAdded = notLoadedIds.map((id) => id.split('@')[0]);
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 ignores version 🐞 Bug ☼ Reliability

WorkspaceAspectsLoader.loadAspectsWithSpan() de-duplicates in-flight loads using id-without-version
(id.split('@')[0]), so a request for foo@2.0.0 can be dropped while foo@1.0.0 is still loading and
the caller returns without ensuring the requested version is loaded. This can leave Harmony missing
required aspects or using an unintended version, causing non-deterministic runtime failures during
aspect/env resolution.
Agent Prompt
## Issue description
`WorkspaceAspectsLoader.loadAspectsWithSpan()` uses `workspace.inFlightAspectsLoads` keyed by `id.split('@')[0]` (id-without-version). This conflates different versions of the same aspect/env and can cause a request for a different version to be filtered out and never awaited/loaded by the current call.

## Issue Context
- The in-flight set is explicitly documented as storing aspect-ids **without version**.
- The loader itself acknowledges that ids may be requested **with** versions.

## Fix Focus Areas
- Make the circular-load guard version-aware for non-legacy aspects (e.g., key by full id, or use a per-call stack keyed by full id).
- If you still need “ignore version” behavior, scope it narrowly to legacy-core envs (or other explicitly single-instance cases), not globally.
- Consider replacing the Set with a `Map<fullId, Promise>` (or similar) so concurrent requests can *join/await* the in-flight load rather than silently skipping.

### Target code
- scopes/workspace/workspace/workspace-aspects-loader.ts[124-137]
- scopes/workspace/workspace/workspace-aspects-loader.ts[150-154]
- scopes/workspace/workspace/workspace.ts[210-214]

ⓘ 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 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 on lines +244 to +250
private getFallbackDefaultEnv(): Environment {
if (!this.fallbackDefaultEnv) {
this.fallbackDefaultEnv = {
name: 'node',
getCompiler: () => getFallbackTypescriptCompiler(),
__getDescriptor: async () => ({ type: 'node' }),
};

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. Fallback env reports node 🐞 Bug ≡ Correctness

When a legacy core env isn’t loaded yet, EnvsMain falls back to a minimal env whose descriptor is
hard-coded to { type: 'node' } and name: 'node', so non-node legacy envs (react/mdx/html) can
get persisted/displayed as “node”. This env descriptor is persisted into extension data during scope
component recalculation, so the misleading metadata can survive beyond the transient “env not
loaded” state.
Agent Prompt
### Issue description
Legacy core envs that are not installed/loaded fall back to `getFallbackDefaultEnv()`, but that fallback env always reports `name: 'node'` and `__getDescriptor(): { type: 'node' }`. This causes components whose configured env is *not* node (e.g. react/mdx/html legacy core envs) to get an incorrect env descriptor (type/name) that can be persisted into model extension data during component recalculation.

### Issue Context
- The fallback is returned from `EnvsMain.getEnv()` specifically for legacy core env IDs without a version.
- `EnvsMain.calcDescriptor()` is used during scope-side recalculation and then persisted via `upsertExtensionData`.

### Fix Focus Areas
- scopes/envs/envs/environments.main.runtime.ts[237-253]
- scopes/envs/envs/environments.main.runtime.ts[553-564]
- scopes/envs/envs/environments.main.runtime.ts[696-710]
- scopes/scope/scope/scope.main.runtime.ts[329-357]

### Suggested fix
Change the fallback env descriptor/name so it cannot misrepresent itself as a specific env (node). Options:
1. Make the fallback env report a neutral identity (e.g. `name: 'fallback'`, `type: 'fallback'`).
2. Preferably: make the fallback env factory accept the requested env id (without version) and return an env whose `name/type` reflect that env id (or at least `type: 'missing-env'`), while still providing the minimal TS transpiler.

Also ensure the persisted descriptor remains accurate enough for user-facing commands (show/envs/status) and doesn’t mislabel legacy react/mdx/html envs as node.

ⓘ 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 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 on lines +149 to +165
getLinter(context: LinterContext, transformers: EslintConfigTransformer[] = []): Linter {
const tsconfigPath = require.resolve('./typescript/tsconfig.json');
const mergedOptions = {
// @ts-ignore - this is a bug in the @types/eslint types
overrideConfig: getEslintConfig() as ESLintLib.Options,
extensions: context.extensionFormats,
useEslintrc: false,
cwd: __dirname,
fix: !!context.fix,
fixTypes: context.fixTypes as ESLintLib.Options['fixTypes'],
} as ESLintOptions;
const configMutator = new EslintConfigMutator(mergedOptions);
const transformerContext: EslintConfigTransformContext = { fix: !!context.fix };
configMutator.setTsConfig(tsconfigPath);
const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext);
return ESLintLinter.create(afterMutation.raw, { logger: this.logger });
}

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. aspectenv.getlinter() uses eslint 📘 Rule violation ⚙ Maintainability

AspectEnv introduces an ESLint-backed linter (ESLintLinter) and imports eslint, which
reintroduces ESLint-based linting into the repo despite the standard being Oxlint + TypeScript
checks. This can create inconsistent linting behavior and increases maintenance/runtime dependency
on ESLint in core tooling.
Agent Prompt
## Issue description
`AspectEnv` now provides linting via `ESLintLinter` and imports `eslint`, which conflicts with the repo compliance requirement to avoid ESLint-based linting and rely on Oxlint + TypeScript type checking.

## Issue Context
The new `getLinter()` implementation builds an ESLint options object (including `overrideConfig`) and returns `ESLintLinter.create(...)`, making ESLint part of the default tooling surface for this environment.

## Fix Focus Areas
- scopes/harmony/aspect/aspect.env.ts[15-29]
- scopes/harmony/aspect/aspect.env.ts[149-165]

ⓘ 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 0607c7d

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