Skip to content

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

Open
davidfirst wants to merge 97 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 97 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). 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); 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 (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Untyped results indexed assignment 📘 Rule violation ≡ Correctness
Description
listAspectsOfComponent() initializes results as {} and then assigns `results[id.toString()] =
..., which can trigger a TypeScript error (no index signature / implicit any`) under the repo’s
type-checking settings. This would cause npm run lint (Oxlint + tsc --noEmit) to fail.
Code

scopes/workspace/workspace/workspace-aspects-manager.ts[R27-40]

+  async listAspectsOfComponent(pattern?: string): Promise<{ [component: string]: AspectSource[] }> {
+    const getIds = async () => {
+      if (!pattern) return this.workspace.listIds();
+      return this.workspace.idsByPattern(pattern);
+    };
+    const componentIds = await getIds();
+    const results = {};
+    await Promise.all(
+      componentIds.map(async (id) => {
+        const aspectSources = await this.getAspectNamesForComponent(id);
+        results[id.toString()] = aspectSources;
+      })
+    );
+    return results;
Evidence
PR Compliance ID 2 requires the PR to pass the repo’s TypeScript type-checking gate. The new code
declares results as {} and then performs a string-keyed assignment, which is a typical tsc
failure unless results is typed (e.g., Record).

CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript): CLAUDE.md: Code Changes Must Pass Repository Linting and Type Checking (Oxlint + TypeScript)
scopes/workspace/workspace/workspace-aspects-manager.ts[27-40]

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

## Issue description
`listAspectsOfComponent()` builds an object map but declares it as `const results = {}` and then indexes into it with `results[id.toString()] = ...`. With TypeScript strict checks, this commonly fails because `{}` has no index signature.
## Issue Context
This is a newly added file and the error would break the repo lint/typecheck gate (`npm run lint`).
## Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-manager.ts[27-40]

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


2. In-flight guard drops versions ✓ Resolved 🐞 Bug ≡ Correctness
Description
WorkspaceAspectsLoader and ScopeAspectsLoader track in-flight loads by id without version
(id.split('@')[0]), so if an aspect dependency chain requests a different version of an already
in-flight aspect, the nested request is skipped and that version may never be loaded, breaking
aspect/env resolution.
Code

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

+    // 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(id.split('@')[0])
+    );
+    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((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 new in-flight logic uses id.split('@')[0] as the set key, which collapses all versions of the
same aspect id into one. The codebase states component extensions may exist in multiple versions and
are resolved via capsules, so skipping different versions due to an in-flight check can prevent
required versions from loading.

scopes/workspace/workspace/workspace-aspects-loader.ts[125-144]
scopes/scope/scope/scope-aspects-loader.ts[128-151]
scopes/workspace/workspace/workspace-aspects-loader.ts[989-993]

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.loadAspects()` uses `this.workspace.inFlightAspectsLoads` keyed by `id.split('@')[0]` to break circular load chains. This suppresses re-entrant loads not only for the same aspect instance, but also for *different versions* of the same aspect ID. In a dependency chain where an aspect depends on another aspect at a different version (or where indirect deps introduce a different version), the second version can be skipped and never loaded.
A similar pattern exists in `ScopeAspectsLoader.getManifestsGraphRecursively()` via `this.scope.inFlightAspectLoads`.
### Issue Context
This repository explicitly supports loading multiple versions of the same extension (e.g. component extensions / envs) when resolving from capsules; the in-flight suppression should not collapse versions except where single-instance semantics are explicitly intended (legacy core envs).
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[125-144]
- scopes/scope/scope/scope-aspects-loader.ts[128-151]
### Implementation notes
- Key `inFlight*` by the *full string id* (including version) for general aspects.
- If you still need version-agnostic suppression, limit it narrowly to legacy-core env IDs (or explicitly documented single-instance aspects), not all aspects.
- Add a regression test scenario where A@v1 depends on B, B depends on A@v2 (or any cross-version diamond), and ensure both versions can load when allowed.

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


3. Hardcoded dist directory 🐞 Bug ≡ Correctness
Description
WorkspaceAspectsLoader’s fallback for requiring compiled aspects assumes compiled output lives under
/dist/..., which breaks loading when an aspect’s compiler uses a non-default dist dir (supported
by the Compiler interface). In those cases the fallback won’t find the compiled entry and the
original require error is rethrown, preventing aspects/envs from loading even though valid dists
exist.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R724-733]

+  private getDistMain(component: Component, localPath: string): string | undefined {
+    const mainFile = component.state._consumer.mainFile;
+    if (!mainFile) return undefined;
+    const distMain = join(localPath, 'dist', mainFile.replace(/\.(ts|tsx|mts|cts|jsx)$/, '.js'));
+    return fs.pathExistsSync(distMain) ? distMain : undefined;
+  }
+
private async linkIfMissingWorkspaceAspects(aspects: AspectDefinition[]) {
const idsToLink = await Promise.all(
aspects.map(async (aspect) => {
Evidence
The fallback path builder currently hardcodes dist/, but the compiler API supports configurable
dist directories; other code in this repo reads the dist dir from the compiler (getDistDir()),
proving the hardcoded assumption is not generally valid.

scopes/workspace/workspace/workspace-aspects-loader.ts[724-740]
scopes/compilation/compiler/types.ts[45-69]
scopes/compilation/compiler/types.ts[117-124]
scopes/compilation/compiler/compiler.main.runtime.ts[92-97]

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.getDistMain()` builds the fallback path as `<localPath>/dist/<mainFile>.js`. This is not guaranteed: Bit’s compiler contract explicitly allows custom `distDir`/`getDistDir()`. When an aspect is compiled into a different output dir (e.g. `lib/`), the fallback never finds the compiled entry and rethrows the original Node require error, breaking aspect/env loading.
### Issue Context
This fallback runs specifically when Node refuses to load TS sources from `node_modules` (e.g. stale `package.json` main pointing to `.ts`). The intended behavior is to load the already-compiled dist when it exists.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[724-740]
### Implementation notes
- Derive the dist directory from the component’s compiler instead of hardcoding `dist`.
- Example approach: get the env via `this.envs.getOrCalculateEnv(component)` (or similar already available in this class) and read `env.getCompiler?.()?.getDistDir?.() ?? env.getCompiler?.()?.distDir ?? 'dist'`.
- Then compute `join(localPath, distDir, mainFileWithJsExt)`.
- Keep it best-effort: if compiler isn’t available, fall back to the current behavior (`dist/`).

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


View more (3)
4. Installed roots ignore version ✓ Resolved 🐞 Bug ≡ Correctness
Description
WorkspaceAspectsLoader.resolveInstalledAspectRecursively() only treats an aspect as a root when
rootIds contains its fully-versioned string id, but rootIds is derived from workspace.jsonc
configured aspect keys which are typically versionless. This can make installed root aspects/envs
resolve to undefined (no parents) and fail to load from node_modules.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R868-872]

+      if (rootIds.includes(aspectStringId)) {
+        const localPath = await this.workspace.getComponentPackagePath(aspectComponent);
+        this.resolvedInstalledAspects.set(aspectStringId, localPath);
+        return localPath;
+      }
Evidence
Configured aspects come from harmony config keys (typically versionless), but installed resolution
root detection uses exact matching against component.id.toString() (often versioned). Since
installedResolverRootIds includes rootAspectsIds as-is, the root check can fail and return
undefined when the node has no parents, preventing resolution from node_modules.

scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts[284-288]
scopes/workspace/workspace/workspace-aspects-loader.ts[366-376]
scopes/workspace/workspace/workspace-aspects-loader.ts[524-552]
scopes/workspace/workspace/workspace-aspects-loader.ts[868-872]

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()` checks `rootIds.includes(aspectStringId)` where `aspectStringId` is `component.id.toString()` (often versioned), while `rootIds` originates from configured aspects (workspace/scope config keys) which are typically **versionless**. As a result, true root installed aspects may not be recognized as roots and can fail resolution.
### Issue Context
- `rootAspectsIds` comes from `getConfiguredAspects()` (config keys) and is passed into `getInstalledAspectResolver()` as part of `installedResolverRootIds`.
- The resolver’s base-case is an exact string match against the versioned `aspectStringId`.
### Fix Focus Areas
- scopes/workspace/workspace/workspace-aspects-loader.ts[366-376]
- scopes/workspace/workspace/workspace-aspects-loader.ts[524-552]
- scopes/workspace/workspace/workspace-aspects-loader.ts[868-872]
### Suggested change
In `resolveInstalledAspectRecursively()`, treat roots as matching by **id without version** as well, e.g.:
- Compute `aspectIdNoVer = aspectComponent.id.toStringWithoutVersion()`.
- Consider it a root if `rootIds` contains either the exact id, the versionless id, or any `rootId` whose `split('@')[0] === aspectIdNoVer`.
This keeps existing behavior for versioned rootIds (e.g. installed legacy envs) while correctly handling versionless configured roots.

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


5. Legacy env preview may throw ✓ Resolved 🐞 Bug ≡ Correctness
Description
PreviewMain still branches on isUsingCoreEnv() in preview calculations, so components using legacy
core envs can fall into code paths that call getEnvComponent() and throw when the env package isn’t
installed yet.
Code

scopes/preview/preview/preview.main.runtime.ts[R312-336]

+    if (this.isUsingCoreOrLegacyCoreEnv(component)) {
return true;
}
const envComponent = await this.envs.getEnvComponent(component);
return this.doesEnvIncludesOnlyOverview(envComponent);
}
private async calculateUseNameParam(component: Component): Promise<boolean> {
-    if (this.envs.isUsingCoreEnv(component)) {
+    if (this.isUsingCoreOrLegacyCoreEnv(component)) {
return true;
}
const envComponent = await this.envs.getEnvComponent(component);
return this.doesEnvUseNameParam(envComponent);
}
+  /**
+   * 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 {
+    if (this.envs.isUsingCoreEnv(component)) return true;
+    const envId = this.envs.getEnvId(component);
+    return this.envs.isLegacyCoreEnv(envId.split('@')[0]);
+  }
Evidence
This PR changes core envs to be legacy (installed from registry) and adds a helper in PreviewMain
warning that legacy core envs may not be installed yet and that fetching the env component can throw
during component load. At the same time, EnvsMain.isUsingCoreEnv() now only considers DEFAULT_ENV
(empty-env) as core, so any remaining isUsingCoreEnv() branches in PreviewMain no longer protect
legacy core envs from code paths that call getEnvComponent().

scopes/preview/preview/preview.main.runtime.ts[309-336]
scopes/envs/envs/environments.main.runtime.ts[112-113]
scopes/envs/envs/environments.main.runtime.ts[261-268]
scopes/envs/envs/environments.main.runtime.ts[561-568]
scopes/envs/envs/environments.main.runtime.ts[606-620]

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` introduced `isUsingCoreOrLegacyCoreEnv()` and uses it in a couple of places, but other preview calculations still treat only `envs.isUsingCoreEnv(component)` as the special case.
After this PR, `envs.isUsingCoreEnv()` is effectively true only for the new default `teambit.harmony/empty-env`, while former core envs (node/react/etc.) are **legacy core envs** and may be temporarily missing before `bit install`. In that state, calling `envs.getEnvComponent(component)` can throw, but several preview calculations run during component load.
### Issue Context
- `PreviewMain`’s new helper explicitly documents that legacy core envs may not be installed yet and that `getEnvComponent()` can throw during component load.
- `EnvsMain.isUsingCoreEnv()` now only checks `getCoreEnvsIds()`, which this PR changed to return only the default empty-env.
### Fix Focus Areas
- scopes/preview/preview/preview.main.runtime.ts[300-700]
### Suggested fix
1. Replace remaining `this.envs.isUsingCoreEnv(component)` checks in **on-load preview calculations** (e.g. scaling/skip-includes/other env-derived flags) with `this.isUsingCoreOrLegacyCoreEnv(component)`.
2. Where the logic truly requires an env component, add a safe guard: if env is legacy core and not loaded/installed, return a conservative default (or skip env-component-derived computation) instead of throwing.
3. Add a regression test covering a component configured with a legacy-core env id (versionless) before running `bit install`, asserting preview-related calculations don’t throw.

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


6. Wrong aspect parent chosen ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolveInstalledAspectRecursively() resolves an installed aspect by following only the first inbound
graph edge (graph.inEdges(id)[0]) and caches the result (including null). If the chosen parent
doesn’t actually resolve the package on disk while another parent would, the aspect can be treated
as “not resolvable” for the rest of the loader run, breaking aspect/env loading.
Code

scopes/workspace/workspace/workspace-aspects-loader.ts[R884-900]

+      // 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, opts, visiting);
+      if (!parentPath) {
+        this.resolvedInstalledAspects.set(aspectStringId, null);
+        return undefined;
+      }
+      const packageName = this.dependencyResolver.getPackageName(aspectComponent);
+      try {
+        const resolvedPath = resolveFrom(parentPath, [packageName]);
+        const localPath = findRoot(resolvedPath);
+        this.resolvedInstalledAspects.set(aspectStringId, localPath);
+        return localPath;
+      } catch (error: any) {
Evidence
The current implementation selects only the first inbound edge as the parent chain and memoizes
failure as null, so if an aspect has multiple direct dependents and the first parent path doesn’t
contain/resolves the package, later attempts won’t try other parents during the same resolution
pass.

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

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.resolveInstalledAspectRecursively()` picks a single parent using `graph.inEdges(aspectStringId)[0]` and resolves the dependency only through that chain. When there are multiple parents (multiple aspects depend on the same aspect), the chosen parent is order-dependent, and a failure is memoized as `null`, preventing later resolution attempts via another parent within the same loader instance.
### Issue Context
This resolver is used by `getInstalledAspectResolver()` to find the on-disk location of non-workspace aspects via recursive `node_modules` resolution.
### Fix Focus Areas
- Update the resolver to consider **all** immediate parents (all `graph.inEdges(aspectStringId)`), attempting resolution via each parent path until one succeeds.
- Only cache a negative result (`null`) after all candidate parent paths have been tried.
#### Code references
- scopes/workspace/workspace/workspace-aspects-loader.ts[850-915]

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



Remediation recommended

7. Fallback compiler misses mts/cts ✓ Resolved 🐞 Bug ≡ Correctness
Description
getFallbackTypescriptCompiler() only transpiles .ts/.tsx/.jsx, so .mts/.cts sources are
treated as “unsupported” and won’t be transpiled to .js dists. When the fallback env is used (env
not loaded yet), aspects/envs with .mts/.cts mains can still fail to load because the loader
expects these mains to have a compiled .js dist.
Code

scopes/envs/envs/fallback-typescript-compiler.ts[R17-35]

+  const supportedExtensions = ['.ts', '.tsx', '.jsx'];
+  const compilerOptions = {
+    module: ts.ModuleKind.CommonJS,
+    target: ts.ScriptTarget.ES2019,
+    jsx: ts.JsxEmit.React,
+    esModuleInterop: true,
+    sourceMap: false,
+  };
+  const replaceFileExtToJs = (filePath: string): string => filePath.replace(/\.(ts|tsx|jsx)$/, '.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)) && !filePath.endsWith('.d.ts'),
+    getDistPathBySrcPath: (srcPath: string) => `dist/${replaceFileExtToJs(srcPath)}`,
Evidence
The fallback compiler currently limits supported extensions and .js output mapping to ts/tsx/jsx
only, while the workspace aspect loader’s dist-main fallback explicitly treats .mts/.cts as TS
sources that should compile to .js. This mismatch means .mts/.cts files won’t get transpiled
by the fallback compiler even though downstream code expects a .js dist.

scopes/envs/envs/fallback-typescript-compiler.ts[17-35]
scopes/workspace/workspace/workspace-aspects-loader.ts[725-734]

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 TypeScript compiler used by `EnvsMain` (when an env isn’t loaded yet) does not transpile `.mts` and `.cts` files. This conflicts with other loader logic that expects `.mts`/`.cts` mains to resolve to `dist/*.js`, and can keep aspects/envs non-requirable even after fallback compilation.
### Issue Context
The fallback compiler is meant to generate minimal requirable dists when the real env cannot be loaded yet (e.g. before `bit install`). It should cover all TypeScript source extensions that the aspect loading fallback assumes will compile to `.js`.
### Fix Focus Areas
- scopes/envs/envs/fallback-typescript-compiler.ts[17-35]
### What to change
- Include `.mts` and `.cts` in `supportedExtensions`.
- Update the extension rewrite regex to also convert `.mts`/`.cts` to `.js`.
- Update the declaration-file exclusion to also exclude `.d.mts` and `.d.cts` (not only `.d.ts`).
### Expected outcome
Fallback compilation produces `.js` outputs for `.mts`/`.cts` sources, aligning with the loader’s `.mts/.cts -> .js` dist resolution logic.

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


8. Hoisted install skips relink ✓ Resolved 🐞 Bug ☼ Reliability
Description
When using pnpm's hoisted linker, install() omits link: entries and restores core-aspect links only
after a successful installComponents() call. If installComponents() throws, the method rethrows
without restoring links, potentially leaving the workspace in a broken state (links
pruned/overwritten).
Code

scopes/workspace/install/install.main.runtime.ts[R461-469]

+      if (isHoistedLinker) {
+        // the hoisted install got no link: entries (see above), so pnpm may have pruned the
+        // core-aspect links or installed published copies over them. restore the links before
+        // anything (e.g. compilation) requires the linked packages.
+        await createLinks(this.workspace.path, linkedRootDeps, {
+          avoidHardLink: true,
+          skipIfSymlinkValid: true,
+        });
+      }
Evidence
The hoisted-linker path sets linkedDependencies to {} and only restores links after the
try/catch returns a successful installResult. The catch rethrows immediately, so the restore step
is skipped on errors.

scopes/workspace/install/install.main.runtime.ts[422-469]

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

## Issue description
For `nodeLinker === 'hoisted'`, the code intentionally passes `{}` as `linkedDependencies` and then calls `createLinks()` only after `installComponents()` succeeds. If `installComponents()` throws, links may have been pruned/overwritten, but the method rethrows and never restores the links.
### Issue Context
The code comment explicitly notes pnpm may prune core-aspect links or install published copies over them when links aren’t part of the resolution.
### Fix Focus Areas
- scopes/workspace/install/install.main.runtime.ts[422-469]
### Suggested change
Wrap the `installComponents()` call with a `try { ... } finally { ... }` (or add restoration inside the `catch` before rethrow) so that when `isHoistedLinker` is true, `createLinks(this.workspace.path, linkedRootDeps, ...)` runs even on failure.
This ensures failed installs don’t leave the workspace with missing/incorrect core-aspect links.

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


9. Wrong loadCustomEnvs type 🐞 Bug ⚙ Maintainability
Description
ScopeAspectsLoader.getManifestsGraphRecursively()/getManifestsGraphAfterInFlightCheck() type
loadCustomEnvs as string even though the ScopeMain load options define and pass it as a boolean
flag. This makes the scope-loading API contract inconsistent and removes type-safety for a control
flag used in aspect/env loading decisions.
Code

scopes/scope/scope/scope-aspects-loader.ts[R159-166]

+    opts: {
+      packageManagerConfigRootDir?: string;
+      workspaceName?: string;
+      loadCustomEnvs?: string;
+    } = {}
+  ): Promise<{ manifests: ManifestOrAspect[]; potentialPluginsIds: string[] }> {
+    const optsWithDefaults = { loadCustomEnvs: false, ...opts };
const components = await this.getNonLoadedAspects(nonVisitedId, lane);
Evidence
The scope loader options declare loadCustomEnvs as a string, while the public scope loading
options define it as boolean; they represent the same flag and are wired together, so the type
should match to preserve a consistent contract and type-safety.

scopes/scope/scope/scope-aspects-loader.ts[115-177]
scopes/scope/scope/scope.main.runtime.ts[111-130]
scopes/scope/scope/scope.main.runtime.ts[1239-1276]

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

## Issue description
`loadCustomEnvs` is a boolean flag in the ScopeMain loading API, but `ScopeAspectsLoader` currently types it as `string` in the internal options object passed through `getManifestsGraphRecursively()` and `getManifestsGraphAfterInFlightCheck()`. This creates an inconsistent contract and undermines type safety.
### Issue Context
`ScopeMain` defines `loadCustomEnvs?: boolean` and forwards it into `scope.loadAspects()`. The loader then treats it as a truthy flag (`if (optsWithDefaults.loadCustomEnvs) { ... }`), so the internal type should be boolean as well.
### Fix Focus Areas
- scopes/scope/scope/scope-aspects-loader.ts[159-173]

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


View more (2)
10. Legacy env not installable ✓ Resolved 🐞 Bug ≡ Correctness
Description
teambit.harmony/bit-custom-aspect is treated as a legacy core env even though it has no pinned
version, so affected components will surface a NonLoadedEnv issue that instructs users to run `bit
install` even though install cannot add any package for it. This can leave users blocked without an
actionable remediation path.
Code

scopes/envs/envs/legacy-core-envs.ts[R22-28]

+/**
+ * envs that were removed from the core long ago and have no published package to pin.
+ * listed here so they won't be reported as invalid config (external env without version).
+ */
+const OLDER_REMOVED_CORE_ENVS = ['teambit.harmony/bit-custom-aspect'];
+
+const LEGACY_CORE_ENVS_IDS = [...Object.keys(LEGACY_CORE_ENVS_VERSIONS), ...OLDER_REMOVED_CORE_ENVS];
Evidence
The env id teambit.harmony/bit-custom-aspect is explicitly included in the legacy-core-env ids
list, but it has no entry in the pinned-versions map, so getPinnedLegacyCoreEnvVersion() returns
undefined. EnvsMain.addNonLoadedEnvAsComponentIssues() uses the legacy-core-env classification to
emit NonLoadedEnv instead of ExternalEnvWithoutVersion, and NonLoadedEnv’s solution is always
run "bit install". However, InstallMain.addUsedLegacyCoreEnvsToWorkspacePolicy() only installs
legacy core envs when a pinned version exists, so bit install will not fix this env.

scopes/envs/envs/legacy-core-envs.ts[22-54]
scopes/envs/envs/environments.main.runtime.ts[1326-1348]
components/component-issues/non-loaded-env.ts[3-6]
scopes/workspace/install/install.main.runtime.ts[883-907]

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

## Issue description
`teambit.harmony/bit-custom-aspect` is included in the legacy-core-env allowlist even though it has no pinned version/package. This makes Bit treat it as “installable legacy env” and emit a `NonLoadedEnv` issue whose solution is hardcoded to `bit install`, but install will never add anything for this env.
### Issue Context
- `NonLoadedEnv` is tag-blocking and suggests `bit install`.
- Install only auto-adds legacy core envs when a pinned version exists.
### Fix Focus Areas
- Ensure env ids with **no pinned version** are **not** classified as `isLegacyCoreEnv()` for purposes of:
- emitting `NonLoadedEnv` remediation
- auto-install policy addition
- Either:
1) remove `teambit.harmony/bit-custom-aspect` from the legacy-core-env classification entirely so it falls back to `ExternalEnvWithoutVersion` (which suggests `bit env set ...`), or
2) keep a separate list for “ignored for config validation” but do not route them through the legacy-core-env install/nonloaded logic.
- scopes/envs/envs/legacy-core-envs.ts[22-28]
- scopes/envs/envs/environments.main.runtime.ts[1326-1348]
- scopes/workspace/install/install.main.runtime.ts[883-907]
- components/component-issues/non-loaded-env.ts[3-6]

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


11. Ambiguous env version pick ✓ Resolved 🐞 Bug ≡ Correctness
Description
EnvsMain.getEnvId() and getEnvDefinitionByStringId() now fall back to selecting an env-slot entry by
matching only the id without version via Array.find(), which returns the first match. If multiple
versions of the same env are loaded and registered (supported for component extensions), a
versionless env reference can resolve to a non-deterministic/incorrect env version.
Code

scopes/envs/envs/environments.main.runtime.ts[R537-541]

+    // the env may be registered to the slot with a version while the component references it
+    // without one and without an extension entry holding the version (e.g. a config coming from
+    // a lane merge). envs are single-instance per process, so match ignoring the version.
+    const versionlessSlotMatch = this.envSlot.toArray().find(([envId]) => envId.split('@')[0] === envIdFromEnvData);
+    if (versionlessSlotMatch) return versionlessSlotMatch[0];
Evidence
The new fallback path uses a version-agnostic find() over envSlot, which is ambiguous if
multiple versions exist. The repo explicitly supports multi-version extensions in capsules, and env
implementations register themselves into the env slot when loaded, making multiple matching entries
possible.

scopes/envs/envs/environments.main.runtime.ts[499-545]
scopes/envs/envs/environments.main.runtime.ts[1173-1186]
scopes/workspace/workspace/workspace-aspects-loader.ts[985-988]
scopes/harmony/node/node.main.runtime.ts[171-188]
scopes/react/react/react.main.runtime.ts[454-473]

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.getEnvId()` and `getEnvDefinitionByStringId()` resolve a versionless env-id by doing a version-agnostic `Array.find()` over `envSlot.toArray()`. When more than one version of the same env is registered in the slot, this becomes order-dependent and can bind a component to an unintended env version.
## Issue Context
The workspace loader explicitly supports loading the same extension/env in multiple versions (for different components), and each loaded env version registers itself via `envs.registerEnv(...)`. In that scenario, `envSlot` can legitimately contain multiple entries with the same `idWithoutVersion`.
## Fix Focus Areas
- Make versionless fallback deterministic:
- If there is exactly **one** matching version in the slot, use it.
- If there are **multiple** matches, fail fast with an actionable error (or require a version to be present) rather than picking the first.
- Keep the current “ignore version” behavior for **legacy core envs** only, where single-instance semantics are explicitly intended.
- Optionally: add logging/telemetry when the ambiguous fallback path is used.
### Suggested code locations
- scopes/envs/envs/environments.main.runtime.ts[499-545]
- scopes/envs/envs/environments.main.runtime.ts[1173-1186]

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

Comment thread scopes/envs/envs/legacy-core-envs.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 446416e

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

Copy link
Copy Markdown

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

…om-manifest

# Conflicts:
#	.bitmap
#	e2e/harmony/tsconfig-env-mismatch.e2e.ts
#	pnpm-lock.yaml
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 d4bb772

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

qodo-free-for-open-source-projects Bot commented Jul 8, 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 Outdated
Comment thread scopes/workspace/install/install.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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 6de8740

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 150dfe8

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 796f76c

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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