UNPKG

@teambit/workspace

Version:
1,030 lines (938 loc) • 41.4 kB
import pMap from 'p-map'; import { getLatestVersionNumber } from '@teambit/legacy.utils'; import { pMapPool } from '@teambit/toolbox.promise.map-pool'; import { concurrentComponentsLimit } from '@teambit/harmony.modules.concurrency'; import type { Component, InvalidComponent } from '@teambit/component'; import { ComponentFS, Config, State, TagMap } from '@teambit/component'; import type { ComponentID } from '@teambit/component-id'; import { ComponentIdList } from '@teambit/component-id'; import mapSeries from 'p-map-series'; import { compact, fromPairs, groupBy, pick, uniq, uniqBy } from 'lodash'; import type { ComponentLoadOptions as LegacyComponentLoadOptions } from '@teambit/legacy.consumer-component'; import { ComponentNotFoundInPath, ConsumerComponent, Dependencies } from '@teambit/legacy.consumer-component'; import { MissingBitMapComponent } from '@teambit/legacy.bit-map'; import { IssuesClasses } from '@teambit/component-issues'; import { ComponentNotFound } from '@teambit/legacy.scope'; import type { DependencyResolverMain } from '@teambit/dependency-resolver'; import { DependencyResolverAspect } from '@teambit/dependency-resolver'; import type { Logger } from '@teambit/logger'; import type { EnvsMain } from '@teambit/envs'; import { EnvsAspect } from '@teambit/envs'; import { ExtensionDataEntry, ExtensionDataList } from '@teambit/legacy.extension-data'; import type { InMemoryCache } from '@teambit/harmony.modules.in-memory-cache'; import { getMaxSizeForComponents, createInMemoryCache } from '@teambit/harmony.modules.in-memory-cache'; import type { AspectLoaderMain } from '@teambit/aspect-loader'; import type { Workspace } from '../workspace'; import { WorkspaceComponent } from './workspace-component'; import { MergeConfigConflict } from '../exceptions/merge-config-conflict'; type GetManyRes = { components: Component[]; invalidComponents: InvalidComponent[]; }; export type ComponentLoadOptions = LegacyComponentLoadOptions & { loadExtensions?: boolean; executeLoadSlot?: boolean; idsToNotLoadAsAspects?: string[]; loadSeedersAsAspects?: boolean; resolveExtensionsVersions?: boolean; }; type LoadGroup = { workspaceIds: ComponentID[]; scopeIds: ComponentID[] } & LoadGroupMetadata; type LoadGroupMetadata = { core?: boolean; aspects?: boolean; seeders?: boolean; envs?: boolean; }; type GetAndLoadSlotOpts = ComponentLoadOptions & LoadGroupMetadata; type ComponentGetOneOptions = { resolveIdVersion?: boolean; }; type WorkspaceScopeIdsMap = { scopeIds: Map<string, ComponentID>; workspaceIds: Map<string, ComponentID>; }; export type LoadCompAsAspectsOptions = { /** * In case the component we are loading is app, whether to load it as app (in a scope aspects capsule) */ loadApps?: boolean; /** * In case the component we are loading is env, whether to load it as env (in a scope aspects capsule) */ loadEnvs?: boolean; /** * In case the component we are loading is a regular aspect, whether to load it as aspect (in a scope aspects capsule) */ loadAspects?: boolean; idsToNotLoadAsAspects?: string[]; /** * Are this core aspects */ core?: boolean; /** * Are this aspects seeders of the load many operation */ seeders?: boolean; }; export class WorkspaceComponentLoader { private componentsCache: InMemoryCache<Component>; // cache loaded components /** * Cache components that loaded from scope (especially for get many for perf improvements) */ private scopeComponentsCache: InMemoryCache<Component>; /** * Cache extension list for components. used by get many for perf improvements. * And to make sure we load extensions first. */ private componentsExtensionsCache: InMemoryCache<{ extensions: ExtensionDataList; errors: Error[] | undefined; envId: string | undefined; }>; private componentLoadedSelfAsAspects: InMemoryCache<boolean>; // cache loaded components constructor( private workspace: Workspace, private logger: Logger, private dependencyResolver: DependencyResolverMain, private envs: EnvsMain, private aspectLoader: AspectLoaderMain ) { this.componentsCache = createInMemoryCache({ maxSize: getMaxSizeForComponents() }); this.scopeComponentsCache = createInMemoryCache({ maxSize: getMaxSizeForComponents() }); this.componentsExtensionsCache = createInMemoryCache({ maxSize: getMaxSizeForComponents() }); this.componentLoadedSelfAsAspects = createInMemoryCache({ maxSize: getMaxSizeForComponents() }); } async getMany(ids: Array<ComponentID>, loadOpts?: ComponentLoadOptions, throwOnFailure = true): Promise<GetManyRes> { const idsWithoutEmpty = compact(ids); if (!idsWithoutEmpty.length) { return { components: [], invalidComponents: [] }; } const callId = Math.floor(Math.random() * 1000); // generate a random callId to be able to identify the call from the logs this.logger.profileTrace(`getMany-${callId}`); this.logger.setStatusLine(`loading ${ids.length} component(s)`); const loadOptsWithDefaults: ComponentLoadOptions = Object.assign( // We don't want to load extension or execute the load slot at this step // we will do it later // this important for better performance // We don't want to resolveExtensionsVersions as with get many we call aspect merger merge before update dependencies // so we will have the correct versions for extensions already and update them after will resolve wrong versions // in some cases { loadExtensions: false, executeLoadSlot: false, loadSeedersAsAspects: true, resolveExtensionsVersions: false }, loadOpts || {} ); const loadOrCached: { idsToLoad: ComponentID[]; fromCache: Component[] } = { idsToLoad: [], fromCache: [] }; idsWithoutEmpty.forEach((id) => { const componentFromCache = this.getFromCache(id, loadOptsWithDefaults); if (componentFromCache) { loadOrCached.fromCache.push(componentFromCache); } else { loadOrCached.idsToLoad.push(id); } }, loadOrCached); const { components: loadedComponents, invalidComponents } = await this.getAndLoadSlotOrdered( loadOrCached.idsToLoad || [], loadOptsWithDefaults, callId ); invalidComponents.forEach(({ err }) => { if (throwOnFailure) throw err; }); const components = uniqBy([...loadedComponents, ...loadOrCached.fromCache], (comp) => { return comp.id.toString(); }); // this.logger.clearStatusLine(); components.forEach((comp) => { this.saveInCache(comp, { loadExtensions: true, executeLoadSlot: true }); }); const idsWithEmptyStrs = ids.map((id) => id.toString()); const requestedComponents = components.filter( (comp) => idsWithEmptyStrs.includes(comp.id.toString()) || idsWithEmptyStrs.includes(comp.id.toStringWithoutVersion()) ); this.logger.profileTrace(`getMany-${callId}`); this.logger.clearStatusLine(); return { components: requestedComponents, invalidComponents }; } private async getAndLoadSlotOrdered( ids: ComponentID[], loadOpts: ComponentLoadOptions, callId = 0 ): Promise<GetManyRes> { if (!ids?.length) return { components: [], invalidComponents: [] }; const workspaceScopeIdsMap: WorkspaceScopeIdsMap = await this.groupAndUpdateIds(ids); this.logger.profileTrace('buildLoadGroups'); const groupsToHandle = await this.buildLoadGroups(workspaceScopeIdsMap); this.logger.profileTrace('buildLoadGroups'); // prefix your command with "BIT_LOG=*" to see the detailed groups if (process.env.BIT_LOG) { printGroupsToHandle(groupsToHandle, this.logger); } const groupsRes = compact( await mapSeries(groupsToHandle, async (group, index) => { const { scopeIds, workspaceIds, aspects, core, seeders, envs } = group; const groupDesc = `getMany-${callId} group ${index + 1}/${groupsToHandle.length} - ${loadGroupToStr(group)}`; this.logger.profileTrace(groupDesc); if (!workspaceIds.length && !scopeIds.length) { throw new Error('getAndLoadSlotOrdered - group has no ids to load'); } const res = await this.getAndLoadSlot(workspaceIds, scopeIds, { ...loadOpts, core, seeders, aspects, envs }); this.logger.profileTrace(groupDesc); // We don't want to return components that were not asked originally (we do want to load them) if (!group.seeders) return undefined; return res; }) ); const finalRes = groupsRes.reduce( (acc, curr) => { return { components: [...acc.components, ...curr.components], invalidComponents: [...acc.invalidComponents, ...curr.invalidComponents], }; }, { components: [], invalidComponents: [] } ); return finalRes; } private async buildLoadGroups(workspaceScopeIdsMap: WorkspaceScopeIdsMap): Promise<Array<LoadGroup>> { const wsIds = Array.from(workspaceScopeIdsMap.workspaceIds.values()); const scopeIds = Array.from(workspaceScopeIdsMap.scopeIds.values()); const allIds = [...wsIds, ...scopeIds]; const groupedByIsCoreEnvs = groupBy(allIds, (id) => { return this.envs.isCoreEnv(id.toStringWithoutVersion()); }); const nonCoreEnvs = groupedByIsCoreEnvs.false || []; await this.populateScopeAndExtensionsCache(nonCoreEnvs, workspaceScopeIdsMap); const allExtIds: Map<string, ComponentID> = new Map(); nonCoreEnvs.forEach((id) => { const idStr = id.toString(); const fromCache = this.componentsExtensionsCache.get(idStr); if (!fromCache || !fromCache.extensions) { return; } fromCache.extensions.forEach((ext) => { if (!allExtIds.has(ext.stringId) && ext.newExtensionId) { allExtIds.set(ext.stringId, ext.newExtensionId); } }); }); const allExtCompIds = Array.from(allExtIds.values()); await this.populateScopeAndExtensionsCache(allExtCompIds || [], workspaceScopeIdsMap); // const allExtIdsStr = allExtCompIds.map((id) => id.toString()); const envsIdsOfWsComps = new Set<string>(); wsIds.forEach((id) => { const idStr = id.toString(); const fromCache = this.componentsExtensionsCache.get(idStr); if (!fromCache || !fromCache.envId) { return; } const envId = fromCache.envId; if (envId) { envsIdsOfWsComps.add(envId); } }); const groupedByIsEnvOfWsComps = groupBy(allExtCompIds, (id) => { const idStr = id.toString(); const withoutVersion = idStr.split('@')[0]; return envsIdsOfWsComps.has(idStr) || envsIdsOfWsComps.has(withoutVersion); }); const notEnvOfWsCompsStrs = (groupedByIsEnvOfWsComps.false || []).map((id) => id.toString()); const groupedByIsExtOfAnother = groupBy(nonCoreEnvs, (id) => { return notEnvOfWsCompsStrs.includes(id.toString()); }); const extIdsFromTheList = (groupedByIsExtOfAnother.true || []).map((id) => id.toString()); const extsNotFromTheList: ComponentID[] = []; for (const [, id] of allExtIds.entries()) { if (!extIdsFromTheList.includes(id.toString())) { extsNotFromTheList.push(id); } } await this.groupAndUpdateIds(extsNotFromTheList, workspaceScopeIdsMap); const layeredExtFromTheList = this.regroupExtIdsFromTheList(groupedByIsExtOfAnother.true); const layeredExtGroups = layeredExtFromTheList.map((ids) => { return { ids, core: false, aspects: true, seeders: true, envs: false, }; }); const layeredEnvsFromTheList = this.regroupEnvsIdsFromTheList(groupedByIsEnvOfWsComps.true, envsIdsOfWsComps); const layeredEnvsGroups = layeredEnvsFromTheList.map((ids) => { return { ids, core: false, aspects: true, seeders: true, envs: true, }; }); const groupsToHandle = [ // Always load first core envs { ids: groupedByIsCoreEnvs.true || [], core: true, aspects: true, seeders: true, envs: true }, // { ids: groupedByIsEnvOfWsComps.true || [], core: false, aspects: true, seeders: false, envs: true }, ...layeredEnvsGroups, { ids: extsNotFromTheList || [], core: false, aspects: true, seeders: false, envs: false }, ...layeredExtGroups, { ids: groupedByIsExtOfAnother.false || [], core: false, aspects: false, seeders: true, envs: false }, ]; // This is a special use case mostly for the bit core repo const envsOfCoreAspectEnv = ['teambit.harmony/envs/core-aspect-env', 'teambit.harmony/envs/core-aspect-env-jest']; const coreAspectEnvGroup = { ids: [], core: true, aspects: true, seeders: true, envs: true }; layeredEnvsGroups.forEach((group) => { const filteredIds = group.ids.filter((id) => envsOfCoreAspectEnv.includes(id.toStringWithoutVersion())); if (filteredIds.length) { // @ts-ignore coreAspectEnvGroup.ids.push(...filteredIds); } }); if (coreAspectEnvGroup.ids.length) { // enter first in the list groupsToHandle.unshift(coreAspectEnvGroup); } // END of bit repo special use case const groupsByWsScope = groupsToHandle.map((group) => { if (!group.ids?.length) return undefined; const groupedByWsScope = groupBy(group.ids, (id) => { return workspaceScopeIdsMap.workspaceIds.has(id.toString()); }); return { workspaceIds: groupedByWsScope.true || [], scopeIds: groupedByWsScope.false || [], core: group.core, aspects: group.aspects, seeders: group.seeders, envs: group.envs, }; }); return compact(groupsByWsScope); } /** * This function will get a list of envs ids and will regroup them into two groups: * 1. envs that are envs of envs from the group * 2. other envs (envs which are just envs of regular components of the workspace) * For Example: * envsIds: [ReactEnv, NodeEnv, BitEnv] * The env of ReactEnv and NodeEnv is BitEnv * The result will be: * [ [BitEnv], [ReactEnv, NodeEnv] ] * * At the moment this function is not recursive, in the future we might want to make it recursive * @param envIds * @param envsIdsOfWsComps * @returns */ private regroupEnvsIdsFromTheList(envIds: ComponentID[] = [], envsIdsOfWsComps: Set<string>): Array<ComponentID[]> { const envsOfEnvs = new Set<string>(); envIds.forEach((envId) => { const idStr = envId.toString(); const fromCache = this.componentsExtensionsCache.get(idStr); if (!fromCache || !fromCache.extensions) { return; } const envOfEnvId = fromCache.envId; if (envOfEnvId && !envsIdsOfWsComps.has(idStr)) { envsOfEnvs.add(envOfEnvId); } }); const existingEnvsOfEnvs = envIds.filter( (id) => envsOfEnvs.has(id.toString()) || envsOfEnvs.has(id.toStringWithoutVersion()) ); const notExistingEnvsOfEnvs = envIds.filter( (id) => !envsOfEnvs.has(id.toString()) && !envsOfEnvs.has(id.toStringWithoutVersion()) ); return [existingEnvsOfEnvs, notExistingEnvsOfEnvs]; } private regroupExtIdsFromTheList(ids: ComponentID[]): Array<ComponentID[]> { // TODO: implement this function // this should handle a case when you have: // compA that has extA and that extA has extB // in that case we now get the following group: // ids: [extA, extB] // while we need extB to be in a different group before extA return [ids]; } private async getAndLoadSlot( workspaceIds: ComponentID[], scopeIds: ComponentID[], loadOpts: GetAndLoadSlotOpts ): Promise<GetManyRes> { const { workspaceComponents, scopeComponents, invalidComponents } = await this.getComponentsWithoutLoadExtensions( workspaceIds, scopeIds, loadOpts ); // If we are here it means we are on workspace, in that case we don't want to load // aspects of scope components as aspects only aspects of workspace components // const components = workspaceComponents.concat(scopeComponents); const allExtensions: ExtensionDataList[] = workspaceComponents.map((component) => { return component.state._consumer.extensions; }); // Ensure we won't load the same extension many times // We don't want to ignore version here, as we do want to load different extensions with same id but different versions here const mergedExtensions = ExtensionDataList.mergeConfigs(allExtensions, false); const filteredMergeExtensions = mergedExtensions.filter((ext) => { return !loadOpts.idsToNotLoadAsAspects?.includes(ext.stringId); }); if (loadOpts.loadExtensions) { this.logger.profileTrace('loadComponentsExtensions'); await this.workspace.loadComponentsExtensions(filteredMergeExtensions); this.logger.profileTrace('loadComponentsExtensions'); } let wsComponentsWithAspects = workspaceComponents; // if (loadOpts.seeders) { this.logger.profileTrace('executeLoadSlot'); wsComponentsWithAspects = await pMapPool(workspaceComponents, (component) => this.executeLoadSlot(component), { concurrency: concurrentComponentsLimit(), }); this.logger.profileTrace('executeLoadSlot'); await this.warnAboutMisconfiguredEnvs(wsComponentsWithAspects); // } const withAspects = wsComponentsWithAspects.concat(scopeComponents); // It's important to load the workspace components as aspects here // otherwise the envs from the workspace won't be loaded at time // so we will get wrong dependencies from component who uses envs from the workspace this.logger.profileTrace('loadCompsAsAspects'); if (loadOpts.loadSeedersAsAspects || (loadOpts.core && loadOpts.aspects)) { await this.loadCompsAsAspects(workspaceComponents.concat(scopeComponents), { loadApps: true, loadEnvs: true, loadAspects: loadOpts.aspects, core: loadOpts.core, seeders: loadOpts.seeders, idsToNotLoadAsAspects: loadOpts.idsToNotLoadAsAspects, }); } this.logger.profileTrace('loadCompsAsAspects'); return { components: withAspects, invalidComponents }; } // TODO: this is similar to scope.main.runtime loadCompAspects func, we should merge them. async loadCompsAsAspects( components: Component[], opts: LoadCompAsAspectsOptions = { loadApps: true, loadEnvs: true, loadAspects: true } ): Promise<void> { const aspectIds: string[] = []; components.forEach((component) => { const firstTimeToLoad = this.componentLoadedSelfAsAspects.get(component.id.toString()) === undefined; const excluded = opts.idsToNotLoadAsAspects?.includes(component.id.toString()); const isCore = this.aspectLoader.isCoreAspect(component.id.toStringWithoutVersion()); const alreadyLoaded = this.aspectLoader.isAspectLoaded(component.id.toString()); const skipLoading = excluded || isCore || alreadyLoaded || !firstTimeToLoad; if (skipLoading) { return; } const idStr = component.id.toString(); const appData = component.state.aspects.get('teambit.harmony/application'); if (opts.loadApps && appData?.data?.appName) { aspectIds.push(idStr); this.componentLoadedSelfAsAspects.set(idStr, true); } const envsData = component.state.aspects.get(EnvsAspect.id); if (opts.loadEnvs && (envsData?.data?.services || envsData?.data?.self || envsData?.data?.type === 'env')) { aspectIds.push(idStr); this.componentLoadedSelfAsAspects.set(idStr, true); } if (opts.loadAspects && envsData?.data?.type === 'aspect') { aspectIds.push(idStr); this.componentLoadedSelfAsAspects.set(idStr, true); } }); if (!aspectIds.length) return; try { await this.workspace.loadAspects(aspectIds, true, 'self loading aspects', { useScopeAspectsCapsule: true }); } catch (err: any) { this.logger.warn(`failed loading components as aspects for components ${aspectIds.join(', ')}`, err); // we ignore that errors at the moment } } private async populateScopeAndExtensionsCache(ids: ComponentID[], workspaceScopeIdsMap: WorkspaceScopeIdsMap) { return mapSeries(ids, async (id) => { const idStr = id.toString(); let componentFromScope; if (!this.scopeComponentsCache.has(idStr)) { try { // Do not import automatically if it's missing, it will throw an error later componentFromScope = await this.workspace.scope.get(id, undefined, false); if (componentFromScope) { this.scopeComponentsCache.set(idStr, componentFromScope); } // This is fine here, as it will be handled later in the process } catch (err: any) { const wsAspectLoader = this.workspace.getWorkspaceAspectsLoader(); wsAspectLoader.throwWsJsoncAspectNotFoundError(err); this.logger.warn(`populateScopeAndExtensionsCache - failed loading component ${idStr} from scope`, err); } } if (!this.componentsExtensionsCache.has(idStr) && workspaceScopeIdsMap.workspaceIds.has(idStr)) { componentFromScope = componentFromScope || this.scopeComponentsCache.get(idStr); const { extensions, errors, envId } = await this.workspace.componentExtensions( id, componentFromScope, undefined, { loadExtensions: false, } ); this.componentsExtensionsCache.set(idStr, { extensions, errors, envId }); } }); } private async warnAboutMisconfiguredEnvs(components: Component[]) { const allIds = uniq(components.map((component) => this.envs.getEnvId(component))); return Promise.all(allIds.map((envId) => this.workspace.warnAboutMisconfiguredEnv(envId))); } private async groupAndUpdateIds( ids: ComponentID[], existingGroups?: WorkspaceScopeIdsMap ): Promise<WorkspaceScopeIdsMap> { const result: WorkspaceScopeIdsMap = existingGroups || { scopeIds: new Map(), workspaceIds: new Map(), }; await Promise.all( ids.map(async (componentId) => { const inWs = await this.isInWsIncludeDeleted(componentId); if (!inWs) { result.scopeIds.set(componentId.toString(), componentId); return undefined; } const resolvedVersions = this.resolveVersion(componentId); result.workspaceIds.set(resolvedVersions.toString(), resolvedVersions); return undefined; }) ); return result; } private async isInWsIncludeDeleted(componentId: ComponentID): Promise<boolean> { const nonDeletedWsIds = this.workspace.listIds(); const deletedWsIds = await this.workspace.locallyDeletedIds(); const allWsIds = nonDeletedWsIds.concat(deletedWsIds); const inWs = allWsIds.find((id) => id.isEqual(componentId, { ignoreVersion: !componentId.hasVersion() })); return !!inWs; } private async getComponentsWithoutLoadExtensions( workspaceIds: ComponentID[], scopeIds: ComponentID[], loadOpts: GetAndLoadSlotOpts ) { const invalidComponents: InvalidComponent[] = []; const errors: { id: ComponentID; err: Error }[] = []; const loadOptsWithDefaults: ComponentLoadOptions = Object.assign( // We don't want to load extension or execute the load slot at this step // we will do it later // this important for better performance // We don't want to store deps in fs cache, as at this point extensions are not loaded yet // so it might save a wrong deps into the cache { loadExtensions: false, executeLoadSlot: false }, loadOpts || {} ); const idsIndex = {}; workspaceIds.forEach((id) => { idsIndex[id.toString()] = id; }); this.logger.profileTrace('consumer.loadComponents'); const { components: legacyComponents, invalidComponents: legacyInvalidComponents, removedComponents, } = await this.workspace.consumer.loadComponents( ComponentIdList.fromArray(workspaceIds), false, loadOptsWithDefaults ); this.logger.profileTrace('consumer.loadComponents'); const allLegacyComponents = legacyComponents.concat(removedComponents); legacyInvalidComponents.forEach((invalidComponent) => { const entry = { id: idsIndex[invalidComponent.id.toString()], err: invalidComponent.error }; if (ConsumerComponent.isComponentInvalidByErrorType(invalidComponent.error)) { invalidComponents.push(entry); return; } if ( this.isComponentNotExistsError(invalidComponent.error) || invalidComponent.error instanceof ComponentNotFoundInPath ) { errors.push(entry); } }); const getWithCatch = (id, legacyComponent) => { return this.get(id, legacyComponent, undefined, undefined, loadOptsWithDefaults).catch((err) => { if (ConsumerComponent.isComponentInvalidByErrorType(err)) { invalidComponents.push({ id, err, }); return undefined; } if (this.isComponentNotExistsError(err) || err instanceof ComponentNotFoundInPath) { errors.push({ id, err, }); return undefined; } throw err; }); }; // await this.getConsumerComponent(id, loadOpts) const componentsP = pMap( allLegacyComponents, (legacyComponent: ConsumerComponent) => { // const componentsP = Promise.all( // allLegacyComponents.map(async (legacyComponent) => { let id = idsIndex[legacyComponent.id.toString()]; if (!id) { const withoutVersion = idsIndex[legacyComponent.id.toStringWithoutVersion()] || legacyComponent.id; if (withoutVersion) { id = withoutVersion.changeVersion(legacyComponent.id.version); idsIndex[legacyComponent.id.toString()] = id; } } return getWithCatch(id, legacyComponent); }, { concurrency: concurrentComponentsLimit(), } ); errors.forEach((err) => { this.logger.console(`failed loading component ${err.id.toString()}, see full error in debug.log file`); this.logger.warn(`failed loading component ${err.id.toString()}`, err.err); }); const components: Component[] = compact(await componentsP); // Here we need to load many, otherwise we will get wrong overrides dependencies data // as when loading the next batch of components (next group) we won't have the envs loaded try { const scopeComponents = await this.workspace.scope.getMany(scopeIds); // We don't want to load envs as part of this step, they will be loaded later // const scopeComponents = await this.workspace.scope.loadMany(scopeIds, undefined, { // loadApps: false, // loadEnvs: true, // loadCompAspects: false, // }); return { workspaceComponents: components, scopeComponents, invalidComponents, }; } catch (err) { const wsAspectLoader = this.workspace.getWorkspaceAspectsLoader(); wsAspectLoader.throwWsJsoncAspectNotFoundError(err); throw err; } } async getInvalid(ids: Array<ComponentID>): Promise<InvalidComponent[]> { const idsWithoutEmpty = compact(ids); const errors: InvalidComponent[] = []; const longProcessLogger = this.logger.createLongProcessLogger('loading components', ids.length); await mapSeries(idsWithoutEmpty, async (id: ComponentID) => { longProcessLogger.logProgress(id.toString()); try { await this.workspace.consumer.loadComponent(id); } catch (err: any) { if (ConsumerComponent.isComponentInvalidByErrorType(err)) { errors.push({ id, err, }); return; } throw err; } }); return errors; } async get( componentId: ComponentID, legacyComponent?: ConsumerComponent, useCache = true, storeInCache = true, loadOpts?: ComponentLoadOptions, getOpts: ComponentGetOneOptions = { resolveIdVersion: true } ): Promise<Component> { const loadOptsWithDefaults: ComponentLoadOptions = Object.assign( { loadExtensions: true, executeLoadSlot: true }, loadOpts || {} ); const id = getOpts?.resolveIdVersion ? this.resolveVersion(componentId) : componentId; const fromCache = this.getFromCache(id, loadOptsWithDefaults); if (fromCache && useCache) { return fromCache; } let consumerComponent = legacyComponent; const inWs = await this.isInWsIncludeDeleted(id); if (inWs && !consumerComponent) { consumerComponent = await this.getConsumerComponent(id, loadOptsWithDefaults); } // in case of out-of-sync, the id may changed during the load process const updatedId = consumerComponent ? consumerComponent.id : id; const component = await this.loadOne(updatedId, consumerComponent, loadOptsWithDefaults); if (storeInCache) { this.addMultipleEnvsIssueIfNeeded(component); // it's in storeInCache block, otherwise, it wasn't fully loaded this.saveInCache(component, loadOptsWithDefaults); } return component; } async getIfExist(componentId: ComponentID) { try { return await this.get(componentId); } catch (err: any) { if (this.isComponentNotExistsError(err)) { return undefined; } throw err; } } private resolveVersion(componentId: ComponentID): ComponentID { const bitIdWithVersion: ComponentID = getLatestVersionNumber( this.workspace.consumer.bitmapIdsFromCurrentLaneIncludeRemoved, componentId ); const id = bitIdWithVersion.version ? componentId.changeVersion(bitIdWithVersion.version) : componentId; return id; } private addMultipleEnvsIssueIfNeeded(component: Component) { const envs = this.envs.getAllEnvsConfiguredOnComponent(component); const envIds = uniq(envs.map((env) => env.id)); if (envIds.length < 2) { return; } component.state.issues.getOrCreate(IssuesClasses.MultipleEnvs).data = envIds; } clearCache() { this.componentsCache.deleteAll(); this.scopeComponentsCache.deleteAll(); this.componentsExtensionsCache.deleteAll(); this.componentLoadedSelfAsAspects.deleteAll(); } clearComponentCache(id: ComponentID) { const idStr = id.toString(); const cachesToClear = [ this.componentsCache, this.scopeComponentsCache, this.componentsExtensionsCache, this.componentLoadedSelfAsAspects, ]; cachesToClear.forEach((cache) => { for (const cacheKey of cache.keys()) { if (cacheKey === idStr || cacheKey.startsWith(`${idStr}:`)) { cache.delete(cacheKey); } } }); } private async loadOne(id: ComponentID, consumerComponent?: ConsumerComponent, loadOpts?: ComponentLoadOptions) { const idStr = id.toString(); const componentFromScope = this.scopeComponentsCache.has(idStr) ? this.scopeComponentsCache.get(idStr) : await this.workspace.scope.get(id); if (!consumerComponent) { if (!componentFromScope) throw new MissingBitMapComponent(id.toString()); return componentFromScope; } const extErrorsFromCache = this.componentsExtensionsCache.has(idStr) ? this.componentsExtensionsCache.get(idStr) : undefined; const { extensions, errors } = extErrorsFromCache || (await this.workspace.componentExtensions(id, componentFromScope, undefined, { loadExtensions: loadOpts?.loadExtensions, })); if (errors?.some((err) => err instanceof MergeConfigConflict)) { consumerComponent.issues.getOrCreate(IssuesClasses.MergeConfigHasConflict).data = true; } // temporarily mutate consumer component extensions until we remove all direct access from legacy to extensions data // TODO: remove this once we remove all direct access from legacy code to extensions data consumerComponent.extensions = extensions; const state = new State( new Config(consumerComponent), await this.workspace.createAspectList(extensions), ComponentFS.fromVinyls(consumerComponent.files), consumerComponent.dependencies, consumerComponent ); if (componentFromScope) { // Removed by @gilad. do not mutate the component from the scope // componentFromScope.state = state; // const workspaceComponent = WorkspaceComponent.fromComponent(componentFromScope, this.workspace); const workspaceComponent = new WorkspaceComponent( componentFromScope.id, componentFromScope.head, state, componentFromScope.tags, this.workspace ); if (loadOpts?.executeLoadSlot) { return this.executeLoadSlot(workspaceComponent, loadOpts); } // const updatedComp = await this.executeLoadSlot(workspaceComponent, loadOpts); return workspaceComponent; } const newComponent = this.newComponentFromState(id, state); if (!loadOpts?.executeLoadSlot) { return newComponent; } return this.executeLoadSlot(newComponent, loadOpts); } private saveInCache(component: Component, loadOpts?: ComponentLoadOptions): void { const cacheKey = createComponentCacheKey(component.id, loadOpts); this.componentsCache.set(cacheKey, component); } /** * make sure that not only the id-str match, but also the legacy-id. * this is needed because the ComponentID.toString() is the same whether or not the legacy-id has * scope-name, as it includes the defaultScope if the scope is empty. * as a result, when out-of-sync is happening and the id is changed to include scope-name in the * legacy-id, the component is the cache has the old id. */ private getFromCache(componentId: ComponentID, loadOpts?: ComponentLoadOptions): Component | undefined { const bitIdWithVersion: ComponentID = this.resolveVersion(componentId); const id = bitIdWithVersion.version ? componentId.changeVersion(bitIdWithVersion.version) : componentId; const cacheKey = createComponentCacheKey(id, loadOpts); // If we try to look for the cache without load extensions/ without execute load slot // but there is an entry after the load, we want to use it as well. // as we want the component, so if we already loaded it with everything, it's fine. // this sometime relevant for cases with tiny cache size (during tag) const cacheKeyWithTrueLoadOpts = createComponentCacheKey(id, { loadExtensions: true, executeLoadSlot: true }); const fromCache = this.componentsCache.get(cacheKey) || this.componentsCache.get(cacheKeyWithTrueLoadOpts); if (fromCache && fromCache.id.isEqual(id)) { return fromCache; } return undefined; } private async getConsumerComponent( id: ComponentID, loadOpts: ComponentLoadOptions = {} ): Promise<ConsumerComponent | undefined> { loadOpts.originatedFromHarmony = true; try { const { components, removedComponents } = await this.workspace.consumer.loadComponents( ComponentIdList.fromArray([id]), true, loadOpts ); return components?.[0] || removedComponents?.[0]; } catch (err: any) { // don't return undefined for any error. otherwise, if the component is invalid (e.g. main // file is missing) it returns the model component later unexpectedly, or if it's new, it // shows MissingBitMapComponent error incorrectly. if (this.isComponentNotExistsError(err)) { this.logger.debug( `failed loading component "${id.toString()}" from the workspace due to "${err.name}" error\n${err.message}` ); return undefined; } throw err; } } private isComponentNotExistsError(err: Error): boolean { return err instanceof ComponentNotFound || err instanceof MissingBitMapComponent; } private async executeLoadSlot(component: Component, loadOpts?: ComponentLoadOptions) { if (component.state._consumer.removed) { // if it was soft-removed now, the component is not in the FS. loading aspects such as composition ends up with // errors as they try to read component files from the filesystem. return component; } // Special load events which runs from the workspace but should run from the correct aspect // TODO: remove this once those extensions dependent on workspace const envsData = await this.envs.calcDescriptor(component, { skipWarnings: !!this.workspace.inInstallContext }); const wsDeps = component.state._consumer.dependencies.dependencies || []; const modelDeps = component.state._consumer.componentFromModel?.dependencies.dependencies || []; const merged = Dependencies.merge([wsDeps, modelDeps]); const envExtendsDeps = merged.get(); // Move to deps resolver main runtime once we switch ws<> deps resolver direction const policy = await this.dependencyResolver.mergeVariantPolicies( component.config.extensions, component.id, component.state._consumer.files, envExtendsDeps ); const dependenciesList = await this.dependencyResolver.extractDepsFromLegacy(component, policy); const resolvedEnvJsonc = await this.envs.calculateEnvManifest( component, component.state._consumer.files, envExtendsDeps ); if (resolvedEnvJsonc) { // @ts-ignore envsData.resolvedEnvJsonc = resolvedEnvJsonc; } const depResolverData = { packageName: this.dependencyResolver.calcPackageName(component), dependencies: dependenciesList.serialize(), policy: policy.serialize(), componentRangePrefix: this.dependencyResolver.calcComponentRangePrefixByConsumerComponent( component.state._consumer ), }; // Make sure we are adding the envs / deps data first because other on load events might depend on it await Promise.all([ this.upsertExtensionData(component, EnvsAspect.id, envsData), this.upsertExtensionData(component, DependencyResolverAspect.id, depResolverData), ]); // We are updating the component state with the envs and deps data here, so in case we have other slots that depend on this data // they will be able to get it, as it's very common use case that during on load someone want to access to the component env for example const aspectListWithEnvsAndDeps = await this.workspace.createAspectList(component.state.config.extensions); component.state.aspects = aspectListWithEnvsAndDeps; const entries = this.workspace.onComponentLoadSlot.toArray(); await mapSeries(entries, async ([extension, onLoad]) => { const data = await onLoad(component, loadOpts); await this.upsertExtensionData(component, extension, data); // Update the aspect list to have changes happened during the on load slot (new data added above) component.state.aspects.upsertEntry(await this.workspace.resolveComponentId(extension), data); }); return component; } private newComponentFromState(id: ComponentID, state: State): Component { return new WorkspaceComponent(id, null, state, new TagMap(), this.workspace); } private async upsertExtensionData(component: Component, extension: string, data: any) { if (!data) return; const existingExtension = component.state.config.extensions.findExtension(extension); if (existingExtension) { // Only merge top level of extension data Object.assign(existingExtension.data, data); return; } component.state.config.extensions.push(await this.getDataEntry(extension, data)); } private async getDataEntry(extension: string, data: { [key: string]: any }): Promise<ExtensionDataEntry> { // TODO: @gilad we need to refactor the extension data entry api. return new ExtensionDataEntry(undefined, undefined, extension, undefined, data); } } function createComponentCacheKey(id: ComponentID, loadOpts?: ComponentLoadOptions): string { const relevantOpts = pick(loadOpts, ['loadExtensions', 'executeLoadSlot', 'loadDocs', 'loadCompositions']); return `${id.toString()}:${JSON.stringify(sortKeys(relevantOpts ?? {}))}`; } function sortKeys(obj: object) { return fromPairs(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))); } function printGroupsToHandle(groupsToHandle: Array<LoadGroup>, logger: Logger): void { groupsToHandle.forEach((group) => { const { scopeIds, workspaceIds, aspects, core, seeders, envs } = group; logger.console( `workspace-component-loader ~ groupsToHandle ${JSON.stringify( { scopeIds: scopeIds.map((id) => id.toString()), workspaceIds: workspaceIds.map((id) => id.toString()), aspects, core, seeders, envs, }, null, 2 )}` ); }); } function loadGroupToStr(loadGroup: LoadGroup): string { const { scopeIds, workspaceIds, aspects, core, seeders, envs } = loadGroup; const attr: string[] = []; if (aspects) attr.push('aspects'); if (core) attr.push('core'); if (seeders) attr.push('seeders'); if (envs) attr.push('envs'); return `workspaceIds: ${workspaceIds.length}, scopeIds: ${scopeIds.length}, (${attr.join('+')})`; }