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 @@ -583,6 +583,20 @@
"mainFile": "index.ts",
"rootDir": "scopes/workspace/eject"
},
"empty-env": {
"name": "empty-env",
"scope": "",
"version": "",
"defaultScope": "teambit.envs",
"mainFile": "index.ts",
"rootDir": "scopes/envs/empty-env",
"config": {
"teambit.envs/env": {},
"teambit.envs/envs": {
"env": "teambit.envs/env"
}
}
},
"entities/lane-diff": {
"name": "entities/lane-diff",
"scope": "teambit.lanes",
Expand Down
166 changes: 166 additions & 0 deletions docs/removing-core-envs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Removing Core Environments from Bit Core

## Overview

This document describes the implementation for removing **all** core environments from Bit's core binary to reduce its size and node_modules footprint. These environments will become regular dependencies instead of bundled core aspects.

### Environments Being Removed

All of the following environments are being removed from core:

- `teambit.harmony/aspect` - Aspect environment
- `teambit.html/html` - HTML environment
- `teambit.mdx/mdx` - MDX environment
- `teambit.envs/env` - Env environment (for creating custom envs)
- `teambit.mdx/readme` - Readme environment
- `teambit.harmony/bit-custom-aspect` - Custom aspect environment
- `teambit.harmony/node` - Node.js environment
- `teambit.react/react` - React environment
- `teambit.react/react-native` - React Native environment

## Background

**Problem**: Core environments are currently bundled with Bit, increasing binary size and node_modules footprint unnecessarily.

**Challenge**: Components using core envs were saved with IDs without versions (e.g., `teambit.harmony/node`) because they were core aspects. After removal, they need versions like any other dependency.

**Solution**: Implement backward compatibility by assigning versions to legacy core envs in memory during component load, without migrating existing snapshots.

## Implementation Details

### Core Concept

- **Old components** (tagged before removal): Have env IDs stored without versions
- **New components** (tagged after removal): Will store env IDs with versions
- **Version assignment**: Happens in memory during load using latest available version
- **Snapshots**: Remain immutable - no data migration required

### Files Modified

1. **`scopes/harmony/bit/manifests.ts`**

- Removed all core env aspects from manifestsMap:
- AspectAspect
- MDXAspect
- ReadmeAspect
- EnvAspect
- NodeAspect (previously removed)
- ReactAspect (previously removed)
- Commented out imports
- Marked as "Removed from core - now a regular env"

2. **`scopes/envs/envs/environments.main.runtime.ts`**

**Added `getLegacyCoreEnvsIds()` method:**

```typescript
private getLegacyCoreEnvsIds(): string[] {
return [
'teambit.harmony/aspect',
'teambit.html/html',
'teambit.mdx/mdx',
'teambit.envs/env',
'teambit.mdx/readme',
'teambit.harmony/bit-custom-aspect',
'teambit.harmony/node',
'teambit.react/react',
'teambit.react/react-native',
];
}
```

**Updated `getCoreEnvsIds()` method:**

- Now returns only legacy core envs (all envs removed from core)
- Maintains backward compatibility for old components

**Enhanced `resolveEnv()` method:**

- Detects legacy core envs (envs in getLegacyCoreEnvsIds list)
- First tries to find env with version in component's aspects
- Falls back to envSlot to find loaded env version
- Returns ComponentID with version assigned

**Updated `calculateEnvId()` method:**

- For core envs (including legacy), calls `resolveEnv()` to get version
- Ensures legacy envs without versions are properly resolved

## Version Assignment Strategy

When loading a component with a legacy core env:

1. Check if env ID is in legacy core envs list (without version)
2. Search component's aspects for this env with a version
3. If not found, check envSlot for registered env version
4. Assign the found version (latest available)
5. Return ComponentID with version

## Key Design Decisions

- **Option A chosen**: Assign latest available version (matches current behavior)
- **No migration**: Version assignment only in memory, snapshots unchanged
- **Core env treatment**: Legacy envs still treated as core for backward compat
- **Package.json**: Works automatically like any other non-core env
- **Empty env as default**: Created `teambit.envs/empty-env` as new DEFAULT_ENV to replace `teambit.harmony/node`

## Empty Env Solution

To solve the chicken-and-egg problem where the default env (Node) was removed from core but needed during early initialization:

1. **Created `teambit.envs/empty-env`**:

- A minimal empty environment with no compiler, tester, linter, or other tools
- Used as the default fallback when no other env is specified
- Remains in core as a lightweight core aspect

2. **Files Created**:

- `scopes/envs/empty-env/empty-env.bit-env.ts` - Empty env class
- `scopes/envs/empty-env/empty-env.aspect.ts` - Aspect definition
- `scopes/envs/empty-env/index.ts` - Exports

3. **Integration**:
- Added `EmptyEnvAspect` to `manifests.ts`
- Changed `DEFAULT_ENV` from `'teambit.harmony/node'` to `'teambit.envs/empty-env'`
- Set to use `teambit.envs/env` as its own env

This approach ensures:

- No external dependencies needed for the default env
- Minimal binary size impact (just a few lines of code)
- Components without an explicit env can still be loaded
- Backward compatibility maintained

## Testing Considerations

To verify the implementation:

1. Create/load old components with legacy core env references (no version)
2. Verify they load correctly with version assigned
3. Tag/snap new components and verify env ID includes version in snapshot
4. Remove core env packages from repository
5. Add them as regular dependencies in workspace.jsonc
6. Test loading and tagging workflows

## Future Work

Once this change is deployed:

1. Remove all env packages from core repository (aspect, html, mdx, env, readme, bit-custom-aspect, node, react, react-native)
2. Publish them as separate packages
3. Update documentation to reflect they are no longer core envs
4. Most users already use non-core envs, so impact should be minimal

## Important Notes

- **HtmlAspect and bit-custom-aspect**: These were not found in manifests.ts, suggesting they may have been removed previously or exist elsewhere
- **All envs treated equally**: Whether an env has "env" in its name or not (like mdx, aspect, readme), they are all environments that provide functionality to components

## Benefits

- **Reduced binary size**: Core envs no longer bundled with Bit
- **Smaller node_modules**: Only needed envs are installed
- **Backward compatible**: Old components continue to work
- **No breaking changes**: Version assignment is transparent
- **Cleaner architecture**: Clear separation between core aspects and envs
5 changes: 5 additions & 0 deletions scopes/envs/empty-env/empty-env.aspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Aspect } from '@teambit/harmony';

export const EmptyEnvAspect = Aspect.create({
id: 'teambit.envs/empty-env',
});
8 changes: 8 additions & 0 deletions scopes/envs/empty-env/empty-env.env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* EmptyEnv - A minimal empty environment used as the default fallback.
* This env has no compiler, tester, linter, or any other tools configured.
* It's used as the default env when no other env is specified.
*/
export class EmptyEnv {}

export default new EmptyEnv();
17 changes: 17 additions & 0 deletions scopes/envs/empty-env/empty-env.main.runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { EnvsMain } from '@teambit/envs';
import { EnvsAspect } from '@teambit/envs';
import { MainRuntime } from '@teambit/cli';
import { EmptyEnv } from './empty-env.env';
import { EmptyEnvAspect } from './empty-env.aspect';

export class EmptyEnvMain {
static runtime = MainRuntime;
static dependencies = [EnvsAspect];
static async provider([envs]: [EnvsMain]) {
const emptyEnv = new EmptyEnv();
envs.registerEnv(emptyEnv);
return new EmptyEnvMain();
}
}

EmptyEnvAspect.addRuntime(EmptyEnvMain);
3 changes: 3 additions & 0 deletions scopes/envs/empty-env/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { EmptyEnv } from './empty-env.env';
export type { EmptyEnvMain } from './empty-env.main.runtime';
export { EmptyEnvAspect, EmptyEnvAspect as default } from './empty-env.aspect';
48 changes: 42 additions & 6 deletions scopes/envs/envs/environments.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export type EnvCompDescriptor = EnvCompDescriptorProps & {

export type Descriptor = RegularCompDescriptor | EnvCompDescriptor;

export const DEFAULT_ENV = 'teambit.harmony/node';
export const DEFAULT_ENV = 'teambit.envs/empty-env';

export class EnvsMain {
/**
Expand Down Expand Up @@ -229,20 +229,31 @@ export class EnvsMain {
return new EnvDefinition(DEFAULT_ENV, defaultEnv);
}

getCoreEnvsIds(): string[] {
/**
* Returns IDs of legacy core envs that were removed from core.
* These envs were previously bundled with Bit but are now regular dependencies.
* Used for backward compatibility - old components reference these without versions.
*/
private getLegacyCoreEnvsIds(): string[] {
return [
'teambit.harmony/aspect',
'teambit.react/react',
'teambit.harmony/node',
'teambit.react/react-native',
'teambit.html/html',
'teambit.mdx/mdx',
'teambit.envs/env',
'teambit.mdx/readme',
'teambit.harmony/bit-custom-aspect',
'teambit.harmony/node',
'teambit.react/react',
'teambit.react/react-native',
];
}

getCoreEnvsIds(): string[] {
// All core envs have been removed from core and are now regular dependencies.
// Return only legacy core envs for backward compatibility with old components.
return ['teambit.envs/empty-env', ...this.getLegacyCoreEnvsIds()];
}

/**
* compose a new environment from a list of environment transformers.
*/
Expand Down Expand Up @@ -588,11 +599,32 @@ export class EnvsMain {
};
}

/**
* Resolves an env ID to a ComponentID with version.
* For legacy core envs (removed from core), assigns the latest loaded version.
*/
resolveEnv(component: Component, id: string) {
const matchedEntry = component.state.aspects.entries.find((aspectEntry) => {
return id === aspectEntry.id.toString() || id === aspectEntry.id.toString({ ignoreVersion: true });
});

if (matchedEntry?.id) return matchedEntry.id;

// Handle legacy core envs that were removed from core
// Old components have these envs stored without version
const withoutVersion = id.split('@')[0];
if (this.getLegacyCoreEnvsIds().includes(withoutVersion)) {
// Try to find this env in the component's aspects (with version)
const legacyEnvWithVersion = component.state.aspects.entries.find((aspectEntry) => {
return aspectEntry.id.toStringWithoutVersion() === withoutVersion;
});
if (legacyEnvWithVersion) return legacyEnvWithVersion.id;

// Fallback: check if env is registered in slot (loaded from workspace/scope)
const fromSlot = this.envSlot.toArray().find(([envId]) => envId.startsWith(`${withoutVersion}@`));
if (fromSlot) return ComponentID.fromString(fromSlot[0]);
}

return matchedEntry?.id;
}

Expand All @@ -609,8 +641,12 @@ export class EnvsMain {
? ComponentID.fromString(envIdFromEnvsConfig).toStringWithoutVersion()
: undefined;

// Handle core envs (including legacy core envs that were removed from core)
// Legacy envs without version will get resolved via resolveEnv() later
if (envIdFromEnvsConfig && this.isCoreEnv(envIdFromEnvsConfig)) {
return ComponentID.fromString(envIdFromEnvsConfig);
// Try to resolve version for legacy core envs
const resolved = this.resolveEnv(component, envIdFromEnvsConfig);
return resolved ? ComponentID.fromString(resolved.toString()) : ComponentID.fromString(envIdFromEnvsConfig);
}

// in some cases we have the id configured in the teambit.envs/envs but without the version
Expand Down
Loading