Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,20 @@
"mainFile": "index.ts",
"rootDir": "components/legacy/component-list"
},
"component-loader": {
"name": "component-loader",
"scope": "",
"version": "",
"defaultScope": "teambit.component",
"mainFile": "index.ts",
"rootDir": "scopes/component/component-loader",
"config": {
"teambit.harmony/aspect": {},
"teambit.envs/envs": {
"env": "teambit.harmony/aspect"
}
}
},
"component-log": {
"name": "component-log",
"scope": "teambit.component",
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/rewrite-component-loading/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-08
222 changes: 222 additions & 0 deletions openspec/changes/rewrite-component-loading/audit/01-call-sites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Audit 1.1 — Workspace/Consumer Load Method Call Sites

**Scope:** all of `scopes/` and `components/` (excluding `e2e/` and `node_modules`).
**Total call sites:** 88 across 6 method names.
**Generated:** as part of OpenSpec change `rewrite-component-loading`, pre-work task 1.1.

## Method totals

| Method | Sites |
| --------------------------------------- | ----- |
| `workspace.get(` | 33 |
| `workspace.getMany(` | 21 |
| `workspace.list(` | 14 |
| `workspace.listWithInvalid(` | 2 |
| `workspace.listInvalid(` | 0 |
| `workspace.getConsumerComponent(` | 0 |
| `consumer.loadComponents(` | 12 |
| `consumer.loadComponent(` | 4 |
| `consumer.loadComponentFromFileSystem(` | 0 |

## Summary by load shape

| Shape | Count |
| -------------- | ----- |
| `files` | 29 |
| `?` ambiguous | 18 |
| `extensions` | 13 |
| `dependencies` | 10 |
| `identity` | 8 |
| `aspects` | 6 |

The `?` bucket is large enough that we should plan to revisit each one during stage-2 migration; assigning a phase to those sites needs reading surrounding code.

---

## DEPENDENCIES (10)

`workspace.get(`:

- `scopes/dependencies/dependencies/dependencies.main.runtime.ts:222` — `const component = await this.workspace.get(compId);` (missing-packages analysis)
- `scopes/dependencies/dependencies/dependencies.main.runtime.ts:357` — `const component = await this.workspace.get(compId);` (debug deps mode)
- `scopes/dependencies/dependencies/dependencies.main.runtime.ts:391` — `await this.workspace.get(id.changeVersion(logItem.tag || logItem.hash));` (blame deps across versions)
- `scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:590` — internal `consumer.loadComponents` call

`workspace.getMany(`:

- `scopes/workspace/workspace/filter.ts:57` — filter by env
- `scopes/workspace/workspace/filter.ts:70` — filter by modified status
- `scopes/workspace/workspace/filter.ts:94` — filter by code modified
- `scopes/workspace/workspace/filter.ts:107` — filter by complex state
- `scopes/compilation/compiler/workspace-compiler.ts:480` — load for compile (then reload at 483)
- `scopes/compilation/compiler/workspace-compiler.ts:483` — reload after env loading
- `scopes/component/snapping/snapping.main.runtime.ts:1142` — fresh post-cache-clear load

`workspace.list(`:

- `scopes/dependencies/dependencies/dependencies.main.runtime.ts:436` — usage tracking
- `scopes/dependencies/dependencies/dependencies.main.runtime.ts:458` — all-components dep scan
- `scopes/component/snapping/version-maker.ts:113` — auto-tag dep tracking

`consumer.loadComponents(`:

- `scopes/workspace/workspace/auto-tag.ts:19` — auto-tag dep graph

---

## EXTENSIONS (13)

`workspace.get(`:

- `scopes/harmony/aspect/aspect.main.runtime.ts:160` — `component.state.aspects`
- `scopes/harmony/aspect/aspect.main.runtime.ts:173` — debug aspects + componentExtensions/beforeMerge
- `scopes/component/dev-files/dev-files.main.runtime.ts:220` — `loadExtensions: false` (note: caller already opts out; verify shape)
- `scopes/generator/generator/component-generator.ts:257` — envs.hasEnvConfigured / envs.getEnv

`workspace.getMany(`:

- `scopes/harmony/aspect/aspect.main.runtime.ts:139` — unsetAspectsFromComponents
- `scopes/harmony/aspect/aspect.main.runtime.ts:197` — updateAspectsToComponents
- `scopes/harmony/application/application.main.runtime.ts:210` — explicit `loadExtensions: true, executeLoadSlot: true, loadSeedersAsAspects: true`

`workspace.list(`:

- `scopes/component/renaming/renaming.main.runtime.ts:147` — refactor packages across all
- `scopes/component/forking/forking.main.runtime.ts:297` — env detection during fork
- `scopes/component/forking/forking.main.runtime.ts:395` — env detection during multi-fork

---

## ASPECTS (6)

`workspace.getMany(`:

- `scopes/harmony/api-server/api-for-ide.ts:550` — autoTag config diff
- `scopes/harmony/api-server/api-for-ide.ts:552` — locallyDeleted config diff
- `scopes/typescript/typescript/typescript.main.runtime.ts:319` — TS file collection (verify)

`workspace.list(`:

- `scopes/defender/tester/tester.main.runtime.ts:194` — uiWatch test execution
- `scopes/git/ci/ci.main.runtime.ts:345` — verifyWorkspaceStatus → builder.build

---

## FILES (29)

`workspace.get(`:

- `scopes/harmony/api-server/api-for-ide.ts:165` — getMainFilePath (`comp.state._consumer.mainFile`)
- `scopes/harmony/api-server/api-for-ide.ts:319` — getCompFiles (`comp.state.filesystem.files`)
- `scopes/harmony/api-server/api-for-ide.ts:733` — getCompDetails
- `scopes/workspace/install/install.main.runtime.ts:281` — env loading file checks
- `scopes/workspace/install/install.main.runtime.ts:1060` — env.jsonc parse/update
- `scopes/workspace/watcher/watcher.ts:663` — componentMap & relative files
- `scopes/component/remove/remove.main.runtime.ts:284` — getRemoveInfo config
- `scopes/component/renaming/renaming.main.runtime.ts:105` — componentPackageName
- `scopes/component/renaming/renaming.main.runtime.ts:120` — refactor variable/class names in source
- `scopes/component/renaming/renaming.main.runtime.ts:141` — target component files
- `scopes/component/forking/forking.main.runtime.ts:87` — fork file checks
- `scopes/component/forking/forking.main.runtime.ts:152` — fork files
- `scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:689` — internal `consumer.loadComponent`
- `scopes/pipelines/builder/build.cmd.ts:181` — builder.listTasks
- `scopes/component/component-log/component-log.main.runtime.ts:331` — historical version files

`workspace.getMany(`:

- `scopes/workspace/install/install.main.runtime.ts:1324` — comp dirs mapping
- `scopes/workspace/modules/node-modules-linker/codemod-components.ts:58` — codemod relative paths
- `scopes/workspace/modules/node-modules-linker/node-modules-linker.ts:322` — explicit `loadSeedersAsAspects: false, loadExtensions: false`
- `scopes/component/component-compare/component-compare.main.runtime.ts:190` — file diff
- `scopes/component/stash/stash.main.runtime.ts:41` — head + isModified
- `scopes/defender/linter/lint.cmd.ts:148` — lint
- `scopes/defender/formatter/format.cmd.ts:85` — format

`workspace.list(`:

- `scopes/harmony/api-server/api-for-ide.ts:246` — getCompsMetadata
- `scopes/workspace/install/install.main.runtime.ts:850` — \_getAllMissingPackages
- `scopes/workspace/install/install.main.runtime.ts:1097` — getAllComponentsDirs
- `scopes/workspace/install/install.main.runtime.ts:1117` — getComponentsManifests
- `scopes/workspace/install/install.main.runtime.ts:1475` — getAllComponentsDirs (variant)
- `scopes/workspace/workspace-config-files/workspace-config-files.main.runtime.ts:361` — component.json ops

`consumer.loadComponents(`:

- `scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:880` — main-loader internal
- `scopes/workspace/eject/components-ejector.ts:118` — eject prep
- `scopes/component/remove/remove-components.ts:158` — remove load
- `scopes/components/legacy/component-list/components-list.ts:150` — listNewComponents
- `scopes/components/legacy/component-list/components-list.ts:229` — getFromFileSystem

`consumer.loadComponent(`:

- `scopes/components/legacy/component-list/components-list.ts:197` — single load for out-of-sync fix

---

## IDENTITY (8)

`workspace.get(`:

- `scopes/component/deprecation/deprecation.main.runtime.ts:80` — deprecated config marker
- `scopes/component/remove/remove.main.runtime.ts:412` — getHeadIfExists

`workspace.getMany(`:

- `scopes/component/remove/remove.main.runtime.ts:125` — markRemoveComps (uses `_consumer`; verify if files needed)
- `scopes/component/status/status.main.runtime.ts:197` — `opts.showIssues ? full : []`

`consumer.loadComponent(`:

- `scopes/component/component/show/legacy-show/get-consumer-component.ts:8` — recent vs model
- `scopes/scope/importer/import-components.ts:851` — three-way merge

---

## AMBIGUOUS (18) — to revisit during migration

`workspace.get(`:

- `scopes/workspace/install/install.cmd.tsx:111`
- `scopes/workspace/workspace/workspace.main.runtime.ts:243` — LegacyComponentLoader subscriber (conversion path)
- `scopes/workspace/workspace/build-graph-ids-from-fs.ts:185`
- `scopes/workspace/workspace/build-graph-from-fs.ts:188`
- `scopes/workspace/workspace/workspace-aspects-loader.ts:764` — aspect loading; very likely `aspects`
- `scopes/workspace/watcher/watcher.ts:785` — return value unused
- `scopes/workspace/modules/node-modules-linker/codemod-components.ts:82`
- `scopes/generator/generator/generator.main.runtime.ts:329` — generator template aspect
- `scopes/semantics/schema/schema.spec.ts:50` — test only

`workspace.listWithInvalid(`:

- `scopes/component/remove/remove-components.ts:135`
- `scopes/component/status/status.main.runtime.ts:93` — **status command's primary call** (target for stage-1 migration to `dependencies`)

`consumer.loadComponents(`:

- `scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:76`
- `scopes/component/checkout/checkout.main.runtime.ts:470`
- `scopes/scope/export/export.main.runtime.ts:747` — out-of-sync fix; return unused

---

## Notable findings

- **`workspace.listInvalid()` and `workspace.getConsumerComponent()` have zero callers** — both can be deleted as part of this change rather than migrated. Mark for removal in stage 3.
- **`workspace-component-loader.ts:590, 689, 880` are internal calls** within the loader being rewritten — they vanish along with the file in stage 3 (task 9.1).
- **`workspace.main.runtime.ts:243`** is the LegacyComponentLoader subscriber — this is the bridge invoked when legacy code requests a harmony component. It's exactly the conversion path the rewrite eliminates. Migration of this site = the legacy/harmony bridge collapse.
- **The 7 explicit `loadExtensions: false` / `loadSeedersAsAspects: false` callers** (e.g. `node-modules-linker.ts:322`, `dev-files.main.runtime.ts:220`, `install.main.runtime.ts:1324`) confirm the demand for sub-aspect phases — they're already paying to opt out of full hydration.
- **`workspace.list()` is called 14 times** — every one of those is a candidate for `listIds()` if the caller only needs IDs, or a lower phase otherwise. Several install-command callers (`850, 1097, 1117, 1475`) iterate over `comp.state.filesystem.files` only and never touch extensions — `files` phase is sufficient.
- **`api-for-ide.ts` is a hot caller (5 sites)** — the IDE server triggers loads on every IDE event. Lowering the default phase here will give immediate latency wins for IDE flows.

## Migration phase assignment recommendations

| Default phase to switch to | Sites |
| -------------------------- | --------------------------------------------------------------------------------- |
| `identity` | the 2 deprecation/remove sites + 2 consumer.loadComponent legacy show |
| `files` | the 29 already classified + several install/list sites currently doing extra work |
| `dependencies` | the 10 above + `status.main.runtime.ts:93` (currently full hydration) |
| `extensions` | the 13 above only |
| `aspects` | the 6 above only |
| revisit | the 18 ambiguous sites |
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Audit 1.2 — Post-load mutations on `ConsumerComponent` and harmony `Component`

**Goal:** find every site that mutates a loaded component instance after the loader returns it. These are the migration risks: the rewrite eliminates the in-place legacy/harmony bridge mutation (`workspace-component-loader.ts:813`), and downstream code that relies on similar mutations needs to be ported.

Excluded from this audit: constructors (`new ConsumerComponent(...)`, `new Version(...)`), test helpers, and the `AddedComponent` type used by the tracker (which is not a loaded component).

## Mutation sites

### Critical — eliminated by the rewrite itself

**`scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:813`** — `consumerComponent.extensions = extensions;`

- This is the legacy/harmony bridge mutation: after the harmony loader resolves extensions, it writes them back onto the legacy `ConsumerComponent`. The rewrite removes this site entirely (the harmony `Component` becomes the source of truth).
- **Migration:** vanishes when `WorkspaceComponentLoader` is deleted (task 9.1).

### Snapping/tagging — must migrate

**`scopes/component/snapping/snapping.main.runtime.ts:351`** — `consumerComponent.extensions = extensionDataList;`

- During snap/tag, after re-deriving extensions from configs, the new `extensionDataList` is restored onto the legacy component (preserving original `data` fields on each extension first, lines 344–349).
- The very next line (`352`) sets `component.state.aspects` on the harmony component — they're already kept in sync but via two separate writes.
- **Migration:** rewrite as a single write to the harmony `Component` (a method like `component.replaceExtensions(list, { preserveDataFromOriginal: true })`). The legacy view (`component.asLegacy()`) reflects this automatically.

**`scopes/component/snapping/snapping.main.runtime.ts:1108`** — `version.extensions = consumerComponent.extensions;`

- Mutates a `Version` model object during snap, copying extensions from the consumer. This is on the **scope-objects model**, not a loaded component — but it depends on the consumer being mutated by site 351 first.
- **Migration:** same as 351 — once the harmony `Component` is the source of truth, copy extensions from the harmony `Component` directly. Order-of-operations becomes: snap derives extensions → harmony Component is updated → Version model is built from it.

**`scopes/component/snapping/version-maker.ts:524`** — `component.extensions = component.extensions.clone();`

- `emptyBuilderData()`: clones the extensions list and zeros the `builder` extension's `data` so that the next tag/snap doesn't carry stale build artifacts. The variable `component` is **harmony**, not legacy — but the mutation pattern still violates the "phases are additive" invariant of the new model (mutating the extensions of a loaded component changes its `extensions`-phase data).
- **Migration:** introduce `component.withBuilderDataReset()` returning a new component, or move this reset into the snap pipeline as a derived value rather than a mutation.

### Checkout/merge/import — file mutations

**`scopes/component/checkout/checkout-version.ts:89`** — `component.files = modifiedFiles;`

- Checkout merges files between the workspace version and the target version, then writes the merged file list back to the consumer.
- **Migration:** this is post-load mutation that produces the new on-disk state. After the rewrite, the canonical write goes through the file system + `componentLoader.invalidate(id)`; the in-memory mutation becomes unnecessary because the next `loader.get(id, { phase: 'files' })` re-reads from disk.

**`scopes/component/merging/merging.main.runtime.ts:529`** — `legacyComponent.version = id.version;`
**`scopes/component/merging/merging.main.runtime.ts:537`** — `legacyComponent.files = modifiedFiles;`

- Same pattern as checkout: merge writes the new file list and the new version onto the legacy component.
- **Migration:** same as checkout — write to disk, invalidate, reload at the requested phase.

**`scopes/scope/importer/import-components.ts:906`** — `component.files = modifiedFiles;`

- During import three-way merge, the merged file list is written onto the loaded consumer component before being persisted.
- **Migration:** same — write to disk, invalidate, reload.

**`scopes/component/snapping/generate-comp-from-scope.ts:78`** — `consumerComponent.version = version.hash().toString();`

- Synthesizes a snap-from-scope component (no workspace) and assigns the version after the Version object is computed.
- **Migration:** this is a synthesis-from-scope flow, not a workspace load. It can keep the mutation pattern locally because the consumer here is constructed inside the function, never returned to the loader cache. Document as "permitted local mutation, not a loader-cache concern."

## Summary

| Site | Category | Migration |
| ----------------------------------- | -------------------------------- | ------------------------------------ |
| `workspace-component-loader.ts:813` | bridge mutation | deleted with the file (task 9.1) |
| `snapping.main.runtime.ts:351` | extensions re-derive | port to harmony method, single write |
| `snapping.main.runtime.ts:1108` | version-model copy from consumer | derive from harmony, no consumer hop |
| `version-maker.ts:524` | builder data reset | derived value or new method |
| `checkout-version.ts:89` | post-merge file write | write→invalidate→reload |
| `merging.main.runtime.ts:529` | post-merge version write | write→invalidate→reload |
| `merging.main.runtime.ts:537` | post-merge file write | write→invalidate→reload |
| `import-components.ts:906` | post-merge file write | write→invalidate→reload |
| `generate-comp-from-scope.ts:78` | synthesis-only | keep (local construction) |

## Notes

- The "write→invalidate→reload" pattern is the same operation expressed without state coupling. With phased lazy hydration, reload cost is bounded by the requested phase — typically `files`, which is cheap.
- The snapping mutations are the most coupled: extensions, version, and aspects are all mutated together. Keep them grouped during migration so that the harmony equivalent is a single atomic update.
- No mutation of `dependencies` / `devDependencies` / `peerDependencies` was found post-load — those are computed and read but not assigned to the loaded component instance.
Loading