@teambit/workspace 1.0.1017 → 1.0.1019

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,279 @@
1
+ # Component Loading Redesign
2
+
3
+ **Status:** Proposal — under review
4
+ **Last updated:** 2026-06-10 (code references are against `master` @ `59855b104`; line numbers will drift)
5
+
6
+ This document is the source of truth for a multi-phase effort to simplify Bit's component-loading
7
+ mechanism: fewer caches, a staged (lazy) loading pipeline, a single env/aspect load planner, and a
8
+ gradual inversion of the legacy `ConsumerComponent` ↔ Harmony `Component` relationship.
9
+
10
+ Each phase is tracked as an OpenSpec change when it starts. Every PR belonging to this effort must
11
+ link here and update the [Status](#status) section.
12
+
13
+ ---
14
+
15
+ ## 1. Problem statement
16
+
17
+ Component loading is the hottest and most fragile path in Bit. Today it is hard to debug, slow on
18
+ large workspaces, and resistant to change — past fixes have introduced regressions, leading to
19
+ workarounds rather than root-cause fixes (e.g. `loadSeedersAsAspects: false` in
20
+ `scopes/workspace/install/install.main.runtime.ts:1320-1327`, added explicitly to dodge a
21
+ regression).
22
+
23
+ Four root problems, which compound each other:
24
+
25
+ ### 1.1 All-or-nothing loading
26
+
27
+ `workspace.get()` / `scope.get()` always produce a fully-hydrated component: file contents read and
28
+ parsed, dependencies resolved from source, extensions merged from 6-8 sources, env calculated, and
29
+ every `onComponentLoad` slot handler executed (docs, compositions, schema, pkg, preview, dev-files,
30
+ apps). Most callers need a fraction of that.
31
+
32
+ Concrete over-loading examples:
33
+
34
+ - `bit deps usage` loads full components, uses only id + dependency list
35
+ (`scopes/dependencies/dependencies/dependencies.main.runtime.ts:436`).
36
+ - The IDE metadata endpoint loads everything to extract id + env + deprecation flag
37
+ (`scopes/harmony/api-server/api-for-ide.ts:246`).
38
+ - `scope.get()` eagerly loads **all file contents** from the object store inside
39
+ `ModelComponent.toConsumerComponent()`
40
+ (`scopes/scope/objects/models/model-component.ts:1143-1212`), even when no caller reads them.
41
+ - `bit remove` loads full components just to reach `state._consumer` for node_modules cleanup
42
+ (`scopes/component/remove/remove.main.runtime.ts:125`).
43
+ - Forking loads the full workspace to pattern-match ids
44
+ (`scopes/component/forking/forking.main.runtime.ts:297`).
45
+
46
+ Partial-load mechanisms exist but are ad-hoc and underused: `ComponentLoadOptions`
47
+ (`loadDocs`/`loadCompositions`/`loadSeedersAsAspects`/`idsToNotLoadAsAspects`),
48
+ `workspace.listIds()`, `graph.getGraphIds()`.
49
+
50
+ ### 1.2 ~11 uncoordinated caches
51
+
52
+ | Cache | Location | Key | Stores |
53
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------- | -------------------------------------------- |
54
+ | `Repository.cache` | `scopes/scope/objects/objects/repository.ts:42` | object hash | BitObjects (LRU 3000, skips objects > 100KB) |
55
+ | `ScopeComponentLoader.componentsCache` | `scopes/scope/scope/scope-component-loader.ts:15` | id | Harmony Component (LRU 500) |
56
+ | `ScopeComponentLoader.importedComponentsCache` | `scope-component-loader.ts:16` | id | boolean, 30-min TTL |
57
+ | `WorkspaceComponentLoader.componentsCache` | `scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:90` | `id + JSON(loadOpts)` | Harmony Component |
58
+ | `WorkspaceComponentLoader.scopeComponentsCache` | `workspace-component-loader.ts:94` | id | scope Components |
59
+ | `WorkspaceComponentLoader.componentsExtensionsCache` | `workspace-component-loader.ts:99` | id | merged extensions + envId |
60
+ | `WorkspaceComponentLoader.componentLoadedSelfAsAspects` | `workspace-component-loader.ts:105` | id | boolean recursion guard |
61
+ | Legacy `ComponentLoader.componentsCache` | `components/legacy/consumer-component/component-loader.ts:53` | id | ConsumerComponent |
62
+ | Legacy `cacheResolvedDependencies` / `componentFsCache` | `component-loader.ts:56-58` | id | resolved deps (memory + FS) |
63
+ | `SourceRepository.cacheUnBuiltIds` | `components/legacy/scope/repositories/sources.ts:67` | id | ModelComponent, 60s TTL |
64
+ | `PkgMain.manifestCache` | `scopes/pkg/pkg/pkg.main.runtime.ts` | head hash | package manifests |
65
+
66
+ Problems:
67
+
68
+ - The workspace cache key embeds serialized `loadOpts`
69
+ (`createComponentCacheKey`, `workspace-component-loader.ts:990`), so the same component loaded
70
+ with different options is cached as separate opaque blobs.
71
+ - Invalidation requires three coordinated calls (`workspace.ts:829-841`:
72
+ `componentLoader.clearCache` + legacy `clearComponentsCache` + `componentStatusLoader.clearCache`).
73
+ - Cache hits are silent — a stale cache is indistinguishable from a fresh load in the logs.
74
+
75
+ ### 1.3 The legacy roundtrip is the spine, not a shim
76
+
77
+ Every load goes: BitObject → `ModelComponent`/`Version` → `ConsumerComponent` (files eagerly
78
+ hydrated) → Harmony `State` that wraps the ConsumerComponent as `state._consumer`
79
+ (`workspace-component-loader.ts:815-821`, `scopes/scope/scope/scope-component-loader.ts:211-229`).
80
+
81
+ Worse, `executeLoadSlot` **mutates** `_consumer.extensions` mid-load after env/deps are computed
82
+ (`workspace-component-loader.ts:948-951`). The Harmony Component is a façade; the real data lives
83
+ in the legacy object, so neither layer can be simplified independently and the load flow has
84
+ hidden write-backs.
85
+
86
+ ### 1.4 Aspect loading and component loading are mutually recursive, with implicit guards
87
+
88
+ Loading a component requires its env → the env is an aspect → an aspect is a component → which has
89
+ its own env. The guards are scattered and implicit:
90
+
91
+ - `idsToNotLoadAsAspects` passed down to prevent re-entry
92
+ (`scopes/workspace/workspace/workspace-aspects-loader.ts:774-792`)
93
+ - `componentLoadedSelfAsAspects` cache (`workspace-component-loader.ts:457-480`)
94
+ - load-group stratification: core envs → env-of-envs → non-env aspects → seeders
95
+ (`buildLoadGroups`, `workspace-component-loader.ts:185-341`)
96
+ - a _second, different_ implementation of the same ordering on the scope side
97
+ (`groupAspectIdsByEnvOfTheList`, `scopes/scope/scope/scope-aspects-loader.ts:59-86`)
98
+
99
+ And at least 9 places swallow load errors silently — the single biggest reason debugging is
100
+ painful. Notable: `loadCompsAsAspects` logs a warning and continues ("we ignore that errors at the
101
+ moment", `workspace-component-loader.ts:486-489`); `requireAspects` returns `[]` on failure unless
102
+ `throwOnError` (`scope-aspects-loader.ts:337-352`); `ignoreAspectLoadingError` filters ESM errors
103
+ during install (`workspace-aspects-loader.ts:915-922`).
104
+
105
+ ---
106
+
107
+ ## 2. Target architecture
108
+
109
+ ### 2.1 Staged loading (the keystone)
110
+
111
+ Replace the monolithic load with explicit stages. Each stage is separately cacheable and lazily
112
+ triggerable:
113
+
114
+ | Stage | Data | Source | Cost |
115
+ | ----------------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | ---------- |
116
+ | **S0 Identity** | ComponentID, head, version list | `.bitmap` / ModelComponent | ~free |
117
+ | **S1 Record** | Version object: file _paths + hashes_, stored deps, stored extensions, build status | object store | cheap read |
118
+ | **S2 Config/Env** | merged extensions, resolved env id | aspects-merger over S1 + workspace config | medium |
119
+ | **S3 Files** | actual file contents (ComponentFS) | FS (workspace) / object store (scope) | heavy |
120
+ | **S4 Computed** | fresh dependency resolution, onLoad slot data (docs, schema, compositions, …) | needs S2 + S3 + aspect code loaded | heaviest |
121
+
122
+ The `Component` object becomes a **handle created at S0** whose accessors pull stages on demand:
123
+ `component.files()` triggers S3, `component.dependencies()` triggers S4-deps, etc. Existing
124
+ synchronous accessors keep working via eager hydration in the legacy-compatible path; refactored
125
+ callers get laziness for free. This formalizes the ad-hoc flags (`loadDocs`, `loadCompositions`,
126
+ `getGraphIds`) into named stages, and makes laziness the default rather than an opt-out workaround.
127
+
128
+ Two highest-leverage laziness changes:
129
+
130
+ - **Lazy file contents**: `toConsumerComponent` constructs `SourceFile`s with a deferred content
131
+ loader (path + hash + `load()` against the Repository) instead of `Promise.all`-hydrating every
132
+ file. `ComponentFS` already abstracts access. This alone removes the biggest scope-side cost.
133
+ - **Lazy slot execution**: `executeLoadSlot` becomes per-aspect on-demand — docs data computed when
134
+ something asks for docs data — with `getMany` able to prefetch for flows that genuinely need it
135
+ (tag/build).
136
+
137
+ ### 2.2 One cache, keyed by (id, stage), one invalidation event
138
+
139
+ A single `ComponentCacheManager` with three tiers:
140
+
141
+ - **L1 objects** — the existing Repository LRU (keep as-is).
142
+ - **L2 component stages** — keyed `(componentId, stage)`. Replaces the workspace's four caches, the
143
+ scope loader cache, and the legacy ComponentLoader cache.
144
+ - **L3 derived** — per-aspect computed data keyed `(componentId, aspectId)`. Replaces the
145
+ loadOpts-in-the-cache-key hack: partial loads cache the _stages_ they computed instead of a
146
+ distinct full-component blob per options combination.
147
+
148
+ Invalidation becomes one event: `invalidate(id, reason)` clears S1+ for that id (and S2 of
149
+ dependents on config change). Every hit/miss/invalidation logs through one chokepoint.
150
+
151
+ ### 2.3 Invert legacy ownership (incrementally — no big-bang rewrite)
152
+
153
+ 1. **Stop mutating `_consumer` during load.** `executeLoadSlot` writes to Harmony aspect entries
154
+ only; legacy readers of `extensions` go through a merging accessor.
155
+ 2. The staged pipeline owns the data; `ConsumerComponent` becomes a _view_ materialized on demand
156
+ (`component.toLegacy()`) — the inverse of today.
157
+ 3. Migrate `_consumer` call sites opportunistically (heavy users: remove, compiler, snapping).
158
+ Each migration shrinks what `toLegacy()` must materialize.
159
+
160
+ ### 2.4 Detangle env resolution from component loading
161
+
162
+ Computing the **env id** only needs S2 (merged extensions), which only needs S1 + workspace config
163
+ — no file reads, no dep resolution, no aspect code execution.
164
+
165
+ - Extract a standalone **`EnvResolver`**: `resolveEnvId(id) → string`, operating purely on S0-S2
166
+ data, with its own small cache. Replaces `populateScopeAndExtensionsCache` +
167
+ `componentsExtensionsCache`.
168
+ - Loading becomes two phases: **plan** (resolve env ids for all requested components, topo-sort the
169
+ env/aspect closure — the `buildLoadGroups` logic, but on cheap S2 data) and **execute** (load
170
+ aspect code for the closure once, then load components in parallel).
171
+ - The recursion guard becomes an **explicit visited-set in the planner**, replacing four scattered
172
+ caches/flags. The workspace and scope loaders' duplicated ordering logic unifies into this one
173
+ planner.
174
+
175
+ ### 2.5 Debuggability as a feature
176
+
177
+ - **`bit debug-load <id>`** — prints the full load trace: stages run, cache hit/miss per stage, the
178
+ extension-merge table showing which of the 6-8 sources contributed each extension and what won
179
+ (the aspects-merger already computes a `beforeMerge` trace — it's just never surfaced), the
180
+ resolved env and why, and timing per stage and per onLoad handler.
181
+ - **No silent error swallowing.** The catch-and-continue spots attach a `LoadIssue` to the
182
+ component (the `issues` mechanism already exists), so `bit status` shows "env X failed to load:
183
+ …" instead of mysteriously degraded behavior later.
184
+ - **One trace context per load request** — generalize the `callId` pattern
185
+ (`workspace-aspects-loader.ts:98`) so every nested aspect/component load logs under the
186
+ originating request id; `BIT_LOG=*` output reads as a tree instead of interleaved noise.
187
+
188
+ ---
189
+
190
+ ## 3. Phase plan
191
+
192
+ Each phase = a milestone, shipped as **multiple small PRs**, each independently green and
193
+ revertible. Pattern: introduce new mechanism alongside old → migrate → delete old. An OpenSpec
194
+ change is created per phase when it starts (not upfront — later phases will be reshaped by what
195
+ earlier ones teach us).
196
+
197
+ ### Phase 1 — Observability + safety net _(low risk, do first)_
198
+
199
+ - [ ] Trace context: one request id per top-level load, propagated through nested aspect/component loads
200
+ - [ ] `bit debug-load <id>` command (stages, cache hits, merge table, env resolution, timings)
201
+ - [ ] Convert swallowed load errors into component `LoadIssue`s surfaced in `bit status`
202
+ - [ ] Stage-level timing instrumentation (groundwork for the benchmark table)
203
+
204
+ ### Phase 2 — Quick perf wins on existing seams
205
+
206
+ - [ ] Benchmark harness committed + baseline recorded (see §4) — **gate for the rest of the phase**
207
+ - [ ] Lazy file contents in `ModelComponent.toConsumerComponent`
208
+ - [ ] `bit deps usage`: ids + stored deps instead of full load
209
+ - [ ] IDE metadata endpoint (`api-for-ide.ts`): S0-S2-level data only
210
+ - [ ] `bit remove` / forking: drop full-component loads where only ids/paths are used
211
+ - [ ] Default `loadDocs: false, loadCompositions: false` for non-UI flows
212
+
213
+ ### Phase 3 — Cache consolidation
214
+
215
+ - [ ] Introduce `ComponentCacheManager` (unused, with tests)
216
+ - [ ] Migrate the workspace loader's four caches onto it
217
+ - [ ] Migrate scope loader + legacy ComponentLoader caches; single `invalidate(id, reason)` event
218
+ - [ ] Delete the old clear-cache coordination (`workspace.ts:829-841`)
219
+
220
+ ### Phase 4 — Staged loading pipeline
221
+
222
+ - [ ] Formalize S0-S4 stage definitions; Component becomes a stage-pulling handle
223
+ - [ ] `executeLoadSlot` → on-demand per-aspect computation, with prefetch for tag/build
224
+ - [ ] Remove `loadOpts` from cache keys (subsumed by per-stage caching)
225
+
226
+ ### Phase 5 — Env planner + loader unification
227
+
228
+ - [ ] Standalone `EnvResolver` on S0-S2 data
229
+ - [ ] One load planner (plan/execute) replacing `buildLoadGroups` + `groupAspectIdsByEnvOfTheList`
230
+ - [ ] Explicit visited-set recursion handling; delete `componentLoadedSelfAsAspects` / `idsToNotLoadAsAspects`
231
+
232
+ ### Phase 6 — Legacy inversion _(ongoing)_
233
+
234
+ - [ ] Freeze `_consumer` mutation during load
235
+ - [ ] Introduce `component.toLegacy()`; pipeline owns the data
236
+ - [ ] Migrate `_consumer` call sites (remove, compiler, snapping first)
237
+
238
+ ---
239
+
240
+ ## 4. Benchmarks
241
+
242
+ Method: run on this repository's own workspace (large, real). Record wall-time (median of 3 warm
243
+ runs) and peak RSS. Update this table at every phase boundary; any phase that regresses a number
244
+ must explain why before merging.
245
+
246
+ | Milestone | `bit status` | `bit list` | `bit show <comp>` | `bit graph` | Peak RSS |
247
+ | ---------------------- | ------------ | ---------- | ----------------- | ----------- | -------- |
248
+ | Baseline (pre-Phase 2) | — | — | — | — | — |
249
+ | After Phase 2 | — | — | — | — | — |
250
+ | After Phase 3 | — | — | — | — | — |
251
+ | After Phase 4 | — | — | — | — | — |
252
+ | After Phase 5 | — | — | — | — | — |
253
+
254
+ ---
255
+
256
+ ## Status
257
+
258
+ | Phase | State | OpenSpec change | PRs |
259
+ | ----------------------- | ----------- | ------------------------------ | --------------------------------------------------- |
260
+ | 1 — Observability | done | `component-load-observability` | [#10418](https://github.com/teambit/bit/pull/10418) |
261
+ | 2 — Quick perf wins | not started | — | — |
262
+ | 3 — Cache consolidation | not started | — | — |
263
+ | 4 — Staged pipeline | not started | — | — |
264
+ | 5 — Env planner | not started | — | — |
265
+ | 6 — Legacy inversion | not started | — | — |
266
+
267
+ **Log:**
268
+
269
+ - 2026-06-10 — Initial proposal drafted.
270
+ - 2026-06-10 — Phase 1 implemented: `@teambit/harmony.modules.load-trace` module (AsyncLocalStorage
271
+ trace context + spans), trace-prefixed logging via the legacy `BitLogger` chokepoint, stage spans
272
+ across workspace/scope/legacy loaders with cache hit/miss attributes, `LoadFailures` component
273
+ issue (non-tag-blocking) attached at the previously-silent catch sites (central:
274
+ `aspectLoader.handleExtensionLoadingError`), and the `bit debug-load <id>` command (stages/cache
275
+ table, extension-merge sources, env origin, issues; `--json` supported). e2e:
276
+ `load-failures-issue.e2e.ts`, `debug-load.e2e.ts`.
277
+ Span-to-stage mapping for Phase 2 benchmarks: S0=`id-resolution`, S1=`scope-load`/
278
+ `state-from-version`, S2=`extension-merge`+`env-calc`, S3=`consumer-fs-load`,
279
+ S4=`dependency-resolution`+`execute-load-slot`/`on-load:*`.
@@ -0,0 +1,279 @@
1
+ # Component Loading Redesign
2
+
3
+ **Status:** Proposal — under review
4
+ **Last updated:** 2026-06-10 (code references are against `master` @ `59855b104`; line numbers will drift)
5
+
6
+ This document is the source of truth for a multi-phase effort to simplify Bit's component-loading
7
+ mechanism: fewer caches, a staged (lazy) loading pipeline, a single env/aspect load planner, and a
8
+ gradual inversion of the legacy `ConsumerComponent` ↔ Harmony `Component` relationship.
9
+
10
+ Each phase is tracked as an OpenSpec change when it starts. Every PR belonging to this effort must
11
+ link here and update the [Status](#status) section.
12
+
13
+ ---
14
+
15
+ ## 1. Problem statement
16
+
17
+ Component loading is the hottest and most fragile path in Bit. Today it is hard to debug, slow on
18
+ large workspaces, and resistant to change — past fixes have introduced regressions, leading to
19
+ workarounds rather than root-cause fixes (e.g. `loadSeedersAsAspects: false` in
20
+ `scopes/workspace/install/install.main.runtime.ts:1320-1327`, added explicitly to dodge a
21
+ regression).
22
+
23
+ Four root problems, which compound each other:
24
+
25
+ ### 1.1 All-or-nothing loading
26
+
27
+ `workspace.get()` / `scope.get()` always produce a fully-hydrated component: file contents read and
28
+ parsed, dependencies resolved from source, extensions merged from 6-8 sources, env calculated, and
29
+ every `onComponentLoad` slot handler executed (docs, compositions, schema, pkg, preview, dev-files,
30
+ apps). Most callers need a fraction of that.
31
+
32
+ Concrete over-loading examples:
33
+
34
+ - `bit deps usage` loads full components, uses only id + dependency list
35
+ (`scopes/dependencies/dependencies/dependencies.main.runtime.ts:436`).
36
+ - The IDE metadata endpoint loads everything to extract id + env + deprecation flag
37
+ (`scopes/harmony/api-server/api-for-ide.ts:246`).
38
+ - `scope.get()` eagerly loads **all file contents** from the object store inside
39
+ `ModelComponent.toConsumerComponent()`
40
+ (`scopes/scope/objects/models/model-component.ts:1143-1212`), even when no caller reads them.
41
+ - `bit remove` loads full components just to reach `state._consumer` for node_modules cleanup
42
+ (`scopes/component/remove/remove.main.runtime.ts:125`).
43
+ - Forking loads the full workspace to pattern-match ids
44
+ (`scopes/component/forking/forking.main.runtime.ts:297`).
45
+
46
+ Partial-load mechanisms exist but are ad-hoc and underused: `ComponentLoadOptions`
47
+ (`loadDocs`/`loadCompositions`/`loadSeedersAsAspects`/`idsToNotLoadAsAspects`),
48
+ `workspace.listIds()`, `graph.getGraphIds()`.
49
+
50
+ ### 1.2 ~11 uncoordinated caches
51
+
52
+ | Cache | Location | Key | Stores |
53
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------- | -------------------------------------------- |
54
+ | `Repository.cache` | `scopes/scope/objects/objects/repository.ts:42` | object hash | BitObjects (LRU 3000, skips objects > 100KB) |
55
+ | `ScopeComponentLoader.componentsCache` | `scopes/scope/scope/scope-component-loader.ts:15` | id | Harmony Component (LRU 500) |
56
+ | `ScopeComponentLoader.importedComponentsCache` | `scope-component-loader.ts:16` | id | boolean, 30-min TTL |
57
+ | `WorkspaceComponentLoader.componentsCache` | `scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:90` | `id + JSON(loadOpts)` | Harmony Component |
58
+ | `WorkspaceComponentLoader.scopeComponentsCache` | `workspace-component-loader.ts:94` | id | scope Components |
59
+ | `WorkspaceComponentLoader.componentsExtensionsCache` | `workspace-component-loader.ts:99` | id | merged extensions + envId |
60
+ | `WorkspaceComponentLoader.componentLoadedSelfAsAspects` | `workspace-component-loader.ts:105` | id | boolean recursion guard |
61
+ | Legacy `ComponentLoader.componentsCache` | `components/legacy/consumer-component/component-loader.ts:53` | id | ConsumerComponent |
62
+ | Legacy `cacheResolvedDependencies` / `componentFsCache` | `component-loader.ts:56-58` | id | resolved deps (memory + FS) |
63
+ | `SourceRepository.cacheUnBuiltIds` | `components/legacy/scope/repositories/sources.ts:67` | id | ModelComponent, 60s TTL |
64
+ | `PkgMain.manifestCache` | `scopes/pkg/pkg/pkg.main.runtime.ts` | head hash | package manifests |
65
+
66
+ Problems:
67
+
68
+ - The workspace cache key embeds serialized `loadOpts`
69
+ (`createComponentCacheKey`, `workspace-component-loader.ts:990`), so the same component loaded
70
+ with different options is cached as separate opaque blobs.
71
+ - Invalidation requires three coordinated calls (`workspace.ts:829-841`:
72
+ `componentLoader.clearCache` + legacy `clearComponentsCache` + `componentStatusLoader.clearCache`).
73
+ - Cache hits are silent — a stale cache is indistinguishable from a fresh load in the logs.
74
+
75
+ ### 1.3 The legacy roundtrip is the spine, not a shim
76
+
77
+ Every load goes: BitObject → `ModelComponent`/`Version` → `ConsumerComponent` (files eagerly
78
+ hydrated) → Harmony `State` that wraps the ConsumerComponent as `state._consumer`
79
+ (`workspace-component-loader.ts:815-821`, `scopes/scope/scope/scope-component-loader.ts:211-229`).
80
+
81
+ Worse, `executeLoadSlot` **mutates** `_consumer.extensions` mid-load after env/deps are computed
82
+ (`workspace-component-loader.ts:948-951`). The Harmony Component is a façade; the real data lives
83
+ in the legacy object, so neither layer can be simplified independently and the load flow has
84
+ hidden write-backs.
85
+
86
+ ### 1.4 Aspect loading and component loading are mutually recursive, with implicit guards
87
+
88
+ Loading a component requires its env → the env is an aspect → an aspect is a component → which has
89
+ its own env. The guards are scattered and implicit:
90
+
91
+ - `idsToNotLoadAsAspects` passed down to prevent re-entry
92
+ (`scopes/workspace/workspace/workspace-aspects-loader.ts:774-792`)
93
+ - `componentLoadedSelfAsAspects` cache (`workspace-component-loader.ts:457-480`)
94
+ - load-group stratification: core envs → env-of-envs → non-env aspects → seeders
95
+ (`buildLoadGroups`, `workspace-component-loader.ts:185-341`)
96
+ - a _second, different_ implementation of the same ordering on the scope side
97
+ (`groupAspectIdsByEnvOfTheList`, `scopes/scope/scope/scope-aspects-loader.ts:59-86`)
98
+
99
+ And at least 9 places swallow load errors silently — the single biggest reason debugging is
100
+ painful. Notable: `loadCompsAsAspects` logs a warning and continues ("we ignore that errors at the
101
+ moment", `workspace-component-loader.ts:486-489`); `requireAspects` returns `[]` on failure unless
102
+ `throwOnError` (`scope-aspects-loader.ts:337-352`); `ignoreAspectLoadingError` filters ESM errors
103
+ during install (`workspace-aspects-loader.ts:915-922`).
104
+
105
+ ---
106
+
107
+ ## 2. Target architecture
108
+
109
+ ### 2.1 Staged loading (the keystone)
110
+
111
+ Replace the monolithic load with explicit stages. Each stage is separately cacheable and lazily
112
+ triggerable:
113
+
114
+ | Stage | Data | Source | Cost |
115
+ | ----------------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | ---------- |
116
+ | **S0 Identity** | ComponentID, head, version list | `.bitmap` / ModelComponent | ~free |
117
+ | **S1 Record** | Version object: file _paths + hashes_, stored deps, stored extensions, build status | object store | cheap read |
118
+ | **S2 Config/Env** | merged extensions, resolved env id | aspects-merger over S1 + workspace config | medium |
119
+ | **S3 Files** | actual file contents (ComponentFS) | FS (workspace) / object store (scope) | heavy |
120
+ | **S4 Computed** | fresh dependency resolution, onLoad slot data (docs, schema, compositions, …) | needs S2 + S3 + aspect code loaded | heaviest |
121
+
122
+ The `Component` object becomes a **handle created at S0** whose accessors pull stages on demand:
123
+ `component.files()` triggers S3, `component.dependencies()` triggers S4-deps, etc. Existing
124
+ synchronous accessors keep working via eager hydration in the legacy-compatible path; refactored
125
+ callers get laziness for free. This formalizes the ad-hoc flags (`loadDocs`, `loadCompositions`,
126
+ `getGraphIds`) into named stages, and makes laziness the default rather than an opt-out workaround.
127
+
128
+ Two highest-leverage laziness changes:
129
+
130
+ - **Lazy file contents**: `toConsumerComponent` constructs `SourceFile`s with a deferred content
131
+ loader (path + hash + `load()` against the Repository) instead of `Promise.all`-hydrating every
132
+ file. `ComponentFS` already abstracts access. This alone removes the biggest scope-side cost.
133
+ - **Lazy slot execution**: `executeLoadSlot` becomes per-aspect on-demand — docs data computed when
134
+ something asks for docs data — with `getMany` able to prefetch for flows that genuinely need it
135
+ (tag/build).
136
+
137
+ ### 2.2 One cache, keyed by (id, stage), one invalidation event
138
+
139
+ A single `ComponentCacheManager` with three tiers:
140
+
141
+ - **L1 objects** — the existing Repository LRU (keep as-is).
142
+ - **L2 component stages** — keyed `(componentId, stage)`. Replaces the workspace's four caches, the
143
+ scope loader cache, and the legacy ComponentLoader cache.
144
+ - **L3 derived** — per-aspect computed data keyed `(componentId, aspectId)`. Replaces the
145
+ loadOpts-in-the-cache-key hack: partial loads cache the _stages_ they computed instead of a
146
+ distinct full-component blob per options combination.
147
+
148
+ Invalidation becomes one event: `invalidate(id, reason)` clears S1+ for that id (and S2 of
149
+ dependents on config change). Every hit/miss/invalidation logs through one chokepoint.
150
+
151
+ ### 2.3 Invert legacy ownership (incrementally — no big-bang rewrite)
152
+
153
+ 1. **Stop mutating `_consumer` during load.** `executeLoadSlot` writes to Harmony aspect entries
154
+ only; legacy readers of `extensions` go through a merging accessor.
155
+ 2. The staged pipeline owns the data; `ConsumerComponent` becomes a _view_ materialized on demand
156
+ (`component.toLegacy()`) — the inverse of today.
157
+ 3. Migrate `_consumer` call sites opportunistically (heavy users: remove, compiler, snapping).
158
+ Each migration shrinks what `toLegacy()` must materialize.
159
+
160
+ ### 2.4 Detangle env resolution from component loading
161
+
162
+ Computing the **env id** only needs S2 (merged extensions), which only needs S1 + workspace config
163
+ — no file reads, no dep resolution, no aspect code execution.
164
+
165
+ - Extract a standalone **`EnvResolver`**: `resolveEnvId(id) → string`, operating purely on S0-S2
166
+ data, with its own small cache. Replaces `populateScopeAndExtensionsCache` +
167
+ `componentsExtensionsCache`.
168
+ - Loading becomes two phases: **plan** (resolve env ids for all requested components, topo-sort the
169
+ env/aspect closure — the `buildLoadGroups` logic, but on cheap S2 data) and **execute** (load
170
+ aspect code for the closure once, then load components in parallel).
171
+ - The recursion guard becomes an **explicit visited-set in the planner**, replacing four scattered
172
+ caches/flags. The workspace and scope loaders' duplicated ordering logic unifies into this one
173
+ planner.
174
+
175
+ ### 2.5 Debuggability as a feature
176
+
177
+ - **`bit debug-load <id>`** — prints the full load trace: stages run, cache hit/miss per stage, the
178
+ extension-merge table showing which of the 6-8 sources contributed each extension and what won
179
+ (the aspects-merger already computes a `beforeMerge` trace — it's just never surfaced), the
180
+ resolved env and why, and timing per stage and per onLoad handler.
181
+ - **No silent error swallowing.** The catch-and-continue spots attach a `LoadIssue` to the
182
+ component (the `issues` mechanism already exists), so `bit status` shows "env X failed to load:
183
+ …" instead of mysteriously degraded behavior later.
184
+ - **One trace context per load request** — generalize the `callId` pattern
185
+ (`workspace-aspects-loader.ts:98`) so every nested aspect/component load logs under the
186
+ originating request id; `BIT_LOG=*` output reads as a tree instead of interleaved noise.
187
+
188
+ ---
189
+
190
+ ## 3. Phase plan
191
+
192
+ Each phase = a milestone, shipped as **multiple small PRs**, each independently green and
193
+ revertible. Pattern: introduce new mechanism alongside old → migrate → delete old. An OpenSpec
194
+ change is created per phase when it starts (not upfront — later phases will be reshaped by what
195
+ earlier ones teach us).
196
+
197
+ ### Phase 1 — Observability + safety net _(low risk, do first)_
198
+
199
+ - [ ] Trace context: one request id per top-level load, propagated through nested aspect/component loads
200
+ - [ ] `bit debug-load <id>` command (stages, cache hits, merge table, env resolution, timings)
201
+ - [ ] Convert swallowed load errors into component `LoadIssue`s surfaced in `bit status`
202
+ - [ ] Stage-level timing instrumentation (groundwork for the benchmark table)
203
+
204
+ ### Phase 2 — Quick perf wins on existing seams
205
+
206
+ - [ ] Benchmark harness committed + baseline recorded (see §4) — **gate for the rest of the phase**
207
+ - [ ] Lazy file contents in `ModelComponent.toConsumerComponent`
208
+ - [ ] `bit deps usage`: ids + stored deps instead of full load
209
+ - [ ] IDE metadata endpoint (`api-for-ide.ts`): S0-S2-level data only
210
+ - [ ] `bit remove` / forking: drop full-component loads where only ids/paths are used
211
+ - [ ] Default `loadDocs: false, loadCompositions: false` for non-UI flows
212
+
213
+ ### Phase 3 — Cache consolidation
214
+
215
+ - [ ] Introduce `ComponentCacheManager` (unused, with tests)
216
+ - [ ] Migrate the workspace loader's four caches onto it
217
+ - [ ] Migrate scope loader + legacy ComponentLoader caches; single `invalidate(id, reason)` event
218
+ - [ ] Delete the old clear-cache coordination (`workspace.ts:829-841`)
219
+
220
+ ### Phase 4 — Staged loading pipeline
221
+
222
+ - [ ] Formalize S0-S4 stage definitions; Component becomes a stage-pulling handle
223
+ - [ ] `executeLoadSlot` → on-demand per-aspect computation, with prefetch for tag/build
224
+ - [ ] Remove `loadOpts` from cache keys (subsumed by per-stage caching)
225
+
226
+ ### Phase 5 — Env planner + loader unification
227
+
228
+ - [ ] Standalone `EnvResolver` on S0-S2 data
229
+ - [ ] One load planner (plan/execute) replacing `buildLoadGroups` + `groupAspectIdsByEnvOfTheList`
230
+ - [ ] Explicit visited-set recursion handling; delete `componentLoadedSelfAsAspects` / `idsToNotLoadAsAspects`
231
+
232
+ ### Phase 6 — Legacy inversion _(ongoing)_
233
+
234
+ - [ ] Freeze `_consumer` mutation during load
235
+ - [ ] Introduce `component.toLegacy()`; pipeline owns the data
236
+ - [ ] Migrate `_consumer` call sites (remove, compiler, snapping first)
237
+
238
+ ---
239
+
240
+ ## 4. Benchmarks
241
+
242
+ Method: run on this repository's own workspace (large, real). Record wall-time (median of 3 warm
243
+ runs) and peak RSS. Update this table at every phase boundary; any phase that regresses a number
244
+ must explain why before merging.
245
+
246
+ | Milestone | `bit status` | `bit list` | `bit show <comp>` | `bit graph` | Peak RSS |
247
+ | ---------------------- | ------------ | ---------- | ----------------- | ----------- | -------- |
248
+ | Baseline (pre-Phase 2) | — | — | — | — | — |
249
+ | After Phase 2 | — | — | — | — | — |
250
+ | After Phase 3 | — | — | — | — | — |
251
+ | After Phase 4 | — | — | — | — | — |
252
+ | After Phase 5 | — | — | — | — | — |
253
+
254
+ ---
255
+
256
+ ## Status
257
+
258
+ | Phase | State | OpenSpec change | PRs |
259
+ | ----------------------- | ----------- | ------------------------------ | --------------------------------------------------- |
260
+ | 1 — Observability | done | `component-load-observability` | [#10418](https://github.com/teambit/bit/pull/10418) |
261
+ | 2 — Quick perf wins | not started | — | — |
262
+ | 3 — Cache consolidation | not started | — | — |
263
+ | 4 — Staged pipeline | not started | — | — |
264
+ | 5 — Env planner | not started | — | — |
265
+ | 6 — Legacy inversion | not started | — | — |
266
+
267
+ **Log:**
268
+
269
+ - 2026-06-10 — Initial proposal drafted.
270
+ - 2026-06-10 — Phase 1 implemented: `@teambit/harmony.modules.load-trace` module (AsyncLocalStorage
271
+ trace context + spans), trace-prefixed logging via the legacy `BitLogger` chokepoint, stage spans
272
+ across workspace/scope/legacy loaders with cache hit/miss attributes, `LoadFailures` component
273
+ issue (non-tag-blocking) attached at the previously-silent catch sites (central:
274
+ `aspectLoader.handleExtensionLoadingError`), and the `bit debug-load <id>` command (stages/cache
275
+ table, extension-merge sources, env origin, issues; `--json` supported). e2e:
276
+ `load-failures-issue.e2e.ts`, `debug-load.e2e.ts`.
277
+ Span-to-stage mapping for Phase 2 benchmarks: S0=`id-resolution`, S1=`scope-load`/
278
+ `state-from-version`, S2=`extension-merge`+`env-calc`, S3=`consumer-fs-load`,
279
+ S4=`dependency-resolution`+`execute-load-slot`/`on-load:*`.
@@ -0,0 +1,53 @@
1
+ import type { Command, CommandOptions } from '@teambit/cli';
2
+ import type { ExtensionsOrigin, Workspace } from './workspace';
3
+ type ExtensionSourceRow = {
4
+ extensionId: string;
5
+ winner: ExtensionsOrigin;
6
+ alsoIn: ExtensionsOrigin[];
7
+ };
8
+ type DebugLoadData = {
9
+ id: string;
10
+ trace?: Record<string, any>;
11
+ extensionSources: ExtensionSourceRow[];
12
+ envId?: string;
13
+ envOrigin?: ExtensionsOrigin;
14
+ issues: Array<{
15
+ type: string;
16
+ description: string;
17
+ data: any;
18
+ }>;
19
+ };
20
+ export declare class DebugLoadCmd implements Command {
21
+ private workspace;
22
+ name: string;
23
+ group: string;
24
+ description: string;
25
+ extendedDescription: string;
26
+ arguments: {
27
+ name: string;
28
+ description: string;
29
+ }[];
30
+ alias: string;
31
+ options: CommandOptions;
32
+ loader: boolean;
33
+ private: boolean;
34
+ constructor(workspace: Workspace);
35
+ report([idStr]: [string]): Promise<string>;
36
+ json([idStr]: [string]): Promise<DebugLoadData>;
37
+ private gatherData;
38
+ /**
39
+ * for every extension in the final (merged) list, find which origins contributed it. the merge
40
+ * gives precedence to the origin that appears first (the order of `beforeMerge`).
41
+ */
42
+ private buildExtensionSources;
43
+ /**
44
+ * the env is taken from the EnvsAspect entry of the merged extensions. the origin that
45
+ * determined it is the first origin (in precedence order) that sets an env.
46
+ */
47
+ private findEnvOrigin;
48
+ private renderStagesSection;
49
+ private renderExtensionSourcesSection;
50
+ private renderEnvSection;
51
+ private renderIssuesSection;
52
+ }
53
+ export {};