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
13 changes: 13 additions & 0 deletions .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,19 @@
"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/envs": {
"env": "teambit.envs/empty-env"
}
}
},
"entities/lane-diff": {
"name": "entities/lane-diff",
"scope": "teambit.lanes",
Expand Down
9 changes: 8 additions & 1 deletion components/legacy/e2e-helper/e2e-command-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,14 @@ export default class CommandHelper {
runCmdOpts?: { envVariables?: Record<string, string> }
) {
const parsedOpts = this.parseOptions(options);
return this.runCmd(`bit install ${packages} ${parsedOpts}`, cwd, 'pipe', undefined, false, runCmdOpts?.envVariables);
return this.runCmd(
`bit install ${packages} ${parsedOpts}`,
cwd,
'pipe',
undefined,
false,
runCmdOpts?.envVariables
);
}
update(flags?: string) {
return this.runCmd(`bit update ${flags || ''}`);
Expand Down
2 changes: 1 addition & 1 deletion components/ui/inputs/lane-selector_1/lane-menu-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { MenuLinkItem } from '@teambit/design.ui.surfaces.menu.link-item';
import { UserAvatar } from '@teambit/design.ui.avatar';
import { TimeAgo } from '@teambit/design.ui.time-ago';
import { Ellipsis } from '@teambit/design.ui.styles.ellipsis';
import { Icon } from '@teambit/design.elements.icon'
import { Icon } from '@teambit/design.elements.icon';
import styles from './lane-menu-item.module.scss';

export type LaneMenuItemProps = {
Expand Down
15 changes: 8 additions & 7 deletions components/ui/models/lanes-model/lanes-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type LaneQueryResult = {
updatedAt?: Date;
updatedBy?: LaneQueryLaneOwner;
dependents?: Array<ComponentIdObj>;
slug?: string
slug?: string;
deleted?: boolean;
};
/**
Expand Down Expand Up @@ -80,7 +80,7 @@ export type LaneModel = {
updatedAt?: Date;
updatedBy?: LaneQueryLaneOwner;
dependents?: ComponentID[];
slug?: string
slug?: string;
deleted?: boolean;
};
/**
Expand Down Expand Up @@ -234,7 +234,7 @@ export class LanesModel {
createdBy,
deleted,
dependents: dependents?.map((dependent) => ComponentID.fromObject(dependent)),
slug
slug,
};
}

Expand Down Expand Up @@ -387,10 +387,11 @@ export class LanesModel {
};

resolveComponentFromUrl = (idFromUrl: string, laneId?: LaneId) => {
const comps = (
(laneId && this.lanes.find((lane) => lane.slug === laneId.toString() || lane.id.isEqual(laneId))) ||
this.viewedLane
)?.components || [];
const comps =
(
(laneId && this.lanes.find((lane) => lane.slug === laneId.toString() || lane.id.isEqual(laneId))) ||
this.viewedLane
)?.components || [];
const includesScope = idFromUrl.includes('.');
if (includesScope) {
return comps.find((component) => component.toStringWithoutVersion() === idFromUrl);
Expand Down
94 changes: 94 additions & 0 deletions docs/removing-core-envs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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. Use `teambit.envs/empty-env` as the new default environment.

## Implementation

### Empty Env Solution

The key challenge was the default env (`teambit.harmony/node`) being removed from core but still needed during initialization. The solution:

**Created `teambit.envs/empty-env`** - a minimal environment with no compiler, tester, or tools that remains in core as a lightweight aspect. This becomes the new `DEFAULT_ENV`.

Files created in `scopes/envs/empty-env/`:

- `empty-env.env.ts` - Empty env class
- `empty-env.aspect.ts` - Aspect definition
- `empty-env.main.runtime.ts` - Runtime registration
- `index.ts` - Type exports

### Backward Compatibility

**`scopes/envs/envs/environments.main.runtime.ts`** - Key changes:

```typescript
// Track all 9 legacy core envs for backward compatibility
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',
];
}

// Include empty-env as a core env along with legacy envs
getCoreEnvsIds(): string[] {
return ['teambit.envs/empty-env', ...this.getLegacyCoreEnvsIds()];
}

// Changed DEFAULT_ENV constant
export const DEFAULT_ENV = 'teambit.envs/empty-env';
```

The `resolveEnv()` method handles version assignment for legacy envs: when loading components with envs lacking versions, it searches the component's aspects for a versioned reference, falls back to envSlot, and assigns the latest available version in memory.

### Core Aspects

**`scopes/harmony/bit/manifests.ts`** - Removed all env aspects from manifestsMap except EmptyEnvAspect:

```typescript
import { EmptyEnvAspect } from '@teambit/empty-env';

export const manifestsMap = {
// ... other aspects
[EmptyEnvAspect.id]: EmptyEnvAspect,
};
```

## Benefits

- **Reduced binary size**: Core envs no longer bundled with Bit
- **Smaller node_modules**: Only needed envs are installed
- **Backward compatible**: Old components without env versions work via automatic version resolution
- **No breaking changes**: Version assignment happens transparently in memory
- **Minimal overhead**: Empty-env adds negligible size to core
25 changes: 16 additions & 9 deletions e2e/harmony/dependencies/allow-scripts.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ chai.use(chaiFs);
helper.command.setConfig('registry', npmCiRegistry.getRegistryUrl());
// The installation below would fail if we didn't explicitly disallow
// @pnpm.e2e/failing-postinstall in allowScripts.
helper.command.install('@pnpm.e2e/failing-postinstall @pnpm.e2e/pre-and-postinstall-scripts-example --disallow-scripts=@pnpm.e2e/failing-postinstall --allow-scripts=@pnpm.e2e/pre-and-postinstall-scripts-example');
helper.command.install(
'@pnpm.e2e/failing-postinstall @pnpm.e2e/pre-and-postinstall-scripts-example --disallow-scripts=@pnpm.e2e/failing-postinstall --allow-scripts=@pnpm.e2e/pre-and-postinstall-scripts-example'
);
workspaceJsonc = helper.workspaceJsonc.read();
});
after(() => {
Expand Down Expand Up @@ -136,14 +138,19 @@ chai.use(chaiFs);
});
// The installation below would fail if we didn't explicitly disallow
// @pnpm.e2e/failing-postinstall in allowScripts.
helper.command.install('@pnpm.e2e/failing-postinstall @pnpm.e2e/pre-and-postinstall-scripts-example', undefined, undefined, {
envVariables: {
BIT_ALLOW_SCRIPTS: JSON.stringify({
'@pnpm.e2e/failing-postinstall': false,
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
}),
},
});
helper.command.install(
'@pnpm.e2e/failing-postinstall @pnpm.e2e/pre-and-postinstall-scripts-example',
undefined,
undefined,
{
envVariables: {
BIT_ALLOW_SCRIPTS: JSON.stringify({
'@pnpm.e2e/failing-postinstall': false,
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
}),
},
}
);
workspaceJsonc = helper.workspaceJsonc.read();
});
after(() => {
Expand Down
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',
});
17 changes: 17 additions & 0 deletions scopes/envs/empty-env/empty-env.env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* 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 {
/**
* mandatory! otherwise, it is not recognized as an env. (see getEnvDescriptorFromEnvDef)
*/
async __getDescriptor() {
return {
type: 'empty',
};
}
}

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