diff --git a/.bitmap b/.bitmap index 0e448925fa11..cd50f9928e72 100644 --- a/.bitmap +++ b/.bitmap @@ -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", diff --git a/docs/removing-core-envs.md b/docs/removing-core-envs.md new file mode 100644 index 000000000000..d43449f42c99 --- /dev/null +++ b/docs/removing-core-envs.md @@ -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 diff --git a/scopes/envs/empty-env/empty-env.aspect.ts b/scopes/envs/empty-env/empty-env.aspect.ts new file mode 100644 index 000000000000..20b4ddfd8ca0 --- /dev/null +++ b/scopes/envs/empty-env/empty-env.aspect.ts @@ -0,0 +1,5 @@ +import { Aspect } from '@teambit/harmony'; + +export const EmptyEnvAspect = Aspect.create({ + id: 'teambit.envs/empty-env', +}); diff --git a/scopes/envs/empty-env/empty-env.env.ts b/scopes/envs/empty-env/empty-env.env.ts new file mode 100644 index 000000000000..873347e9eb13 --- /dev/null +++ b/scopes/envs/empty-env/empty-env.env.ts @@ -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(); diff --git a/scopes/envs/empty-env/empty-env.main.runtime.ts b/scopes/envs/empty-env/empty-env.main.runtime.ts new file mode 100644 index 000000000000..d9f1a9c47369 --- /dev/null +++ b/scopes/envs/empty-env/empty-env.main.runtime.ts @@ -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); diff --git a/scopes/envs/empty-env/index.ts b/scopes/envs/empty-env/index.ts new file mode 100644 index 000000000000..f53dfeded785 --- /dev/null +++ b/scopes/envs/empty-env/index.ts @@ -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'; diff --git a/scopes/envs/envs/environments.main.runtime.ts b/scopes/envs/envs/environments.main.runtime.ts index 86f6bf6f3788..346110835150 100644 --- a/scopes/envs/envs/environments.main.runtime.ts +++ b/scopes/envs/envs/environments.main.runtime.ts @@ -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 { /** @@ -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. */ @@ -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; } @@ -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 diff --git a/scopes/harmony/bit/manifests.ts b/scopes/harmony/bit/manifests.ts index ed28cffbd0f2..d6568898dc2e 100644 --- a/scopes/harmony/bit/manifests.ts +++ b/scopes/harmony/bit/manifests.ts @@ -1,4 +1,4 @@ -import { AspectAspect } from '@teambit/aspect'; +// import { AspectAspect } from '@teambit/aspect'; // Removed from core - now a regular env import { AspectLoaderAspect } from '@teambit/aspect-loader'; import { BuilderAspect } from '@teambit/builder'; import { BundlerAspect } from '@teambit/bundler'; @@ -12,7 +12,7 @@ import { DependencyResolverAspect } from '@teambit/dependency-resolver'; import { DeprecationAspect } from '@teambit/deprecation'; import { DocsAspect } from '@teambit/docs'; import { EnvsAspect } from '@teambit/envs'; -import { EnvAspect } from '@teambit/env'; +// import { EnvAspect } from '@teambit/env'; // Removed from core - now a regular env import { ExpressAspect } from '@teambit/express'; import { YarnAspect } from '@teambit/yarn'; import { GeneratorAspect } from '@teambit/generator'; @@ -23,14 +23,14 @@ import { InsightsAspect } from '@teambit/insights'; import { IsolatorAspect } from '@teambit/isolator'; import { JestAspect } from '@teambit/jest'; import { LoggerAspect } from '@teambit/logger'; -import { NodeAspect } from '@teambit/node'; +// import { NodeAspect } from '@teambit/node'; // Removed from core - now a regular env import { NotificationsAspect } from '@teambit/notifications'; import { PanelUiAspect } from '@teambit/panels'; import { PkgAspect } from '@teambit/pkg'; import { PnpmAspect } from '@teambit/pnpm'; import { PreviewAspect } from '@teambit/preview'; import { ComponentSizerAspect } from '@teambit/component-sizer'; -import { ReactAspect } from '@teambit/react'; +// import { ReactAspect } from '@teambit/react'; // Removed from core - now a regular env import { VueAspect } from '@teambit/vue-aspect'; import { ReactRouterAspect } from '@teambit/react-router'; import { SchemaAspect } from '@teambit/schema'; @@ -61,8 +61,8 @@ import { PrettierAspect } from '@teambit/prettier'; import { WorkerAspect } from '@teambit/worker'; import { GlobalConfigAspect } from '@teambit/global-config'; import { MultiCompilerAspect } from '@teambit/multi-compiler'; -import { MDXAspect } from '@teambit/mdx'; -import { ReadmeAspect } from '@teambit/readme'; +// import { MDXAspect } from '@teambit/mdx'; // Removed from core - now a regular env +// import { ReadmeAspect } from '@teambit/readme'; // Removed from core - now a regular env import { ApplicationAspect } from '@teambit/application'; import { ExportAspect } from '@teambit/export'; import { ImporterAspect } from '@teambit/importer'; @@ -108,6 +108,7 @@ import { ConfigStoreAspect } from '@teambit/config-store'; import { CliMcpServerAspect } from '@teambit/cli-mcp-server'; import { CiAspect } from '@teambit/ci'; import { ScriptsAspect } from '@teambit/scripts'; +import { EmptyEnvAspect } from '@teambit/empty-env'; /** * this is the place to register core aspects. @@ -129,8 +130,8 @@ export const manifestsMap = { [FormatterAspect.id]: FormatterAspect, [ValidatorAspect.id]: ValidatorAspect, [ComponentAspect.id]: ComponentAspect, - [MDXAspect.id]: MDXAspect, - [ReadmeAspect.id]: ReadmeAspect, + // [MDXAspect.id]: MDXAspect, // Removed from core - now a regular env + // [ReadmeAspect.id]: ReadmeAspect, // Removed from core - now a regular env [PreviewAspect.id]: PreviewAspect, [ComponentSizerAspect.id]: ComponentSizerAspect, [DocsAspect.id]: DocsAspect, @@ -143,7 +144,7 @@ export const manifestsMap = { [UIAspect.id]: UIAspect, [GeneratorAspect.id]: GeneratorAspect, [EnvsAspect.id]: EnvsAspect, - [EnvAspect.id]: EnvAspect, + // [EnvAspect.id]: EnvAspect, // Removed from core - now a regular env [GraphAspect.id]: GraphAspect, [PubsubAspect.id]: PubsubAspect, [DependencyResolverAspect.id]: DependencyResolverAspect, @@ -151,7 +152,7 @@ export const manifestsMap = { [IsolatorAspect.id]: IsolatorAspect, [LoggerAspect.id]: LoggerAspect, [PkgAspect.id]: PkgAspect, - [ReactAspect.id]: ReactAspect, + // [ReactAspect.id]: ReactAspect, // Removed from core - now a regular env [VueAspect.id]: VueAspect, [WorkerAspect.id]: WorkerAspect, // [StencilAspect.id]: StencilAspect, @@ -162,14 +163,14 @@ export const manifestsMap = { [VariantsAspect.id]: VariantsAspect, [DeprecationAspect.id]: DeprecationAspect, [ExpressAspect.id]: ExpressAspect, - [AspectAspect.id]: AspectAspect, + // [AspectAspect.id]: AspectAspect, // Removed from core - now a regular env [WebpackAspect.id]: WebpackAspect, [SchemaAspect.id]: SchemaAspect, [ReactRouterAspect.id]: ReactRouterAspect, [TypescriptAspect.id]: TypescriptAspect, [PanelUiAspect.id]: PanelUiAspect, [BabelAspect.id]: BabelAspect, - [NodeAspect.id]: NodeAspect, + // [NodeAspect.id]: NodeAspect, // Removed from core - now a regular env [NotificationsAspect.id]: NotificationsAspect, [BundlerAspect.id]: BundlerAspect, [JestAspect.id]: JestAspect, @@ -224,6 +225,7 @@ export const manifestsMap = { [CliMcpServerAspect.id]: CliMcpServerAspect, [CiAspect.id]: CiAspect, [ScriptsAspect.id]: ScriptsAspect, + [EmptyEnvAspect.id]: EmptyEnvAspect, }; export function isCoreAspect(id: string) {