UNPKG

@teambit/workspace

Version:
348 lines (341 loc) • 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AspectsMerger = void 0; function _envs() { const data = require("@teambit/envs"); _envs = function () { return data; }; return data; } function _dependencyResolver() { const data = require("@teambit/dependency-resolver"); _dependencyResolver = function () { return data; }; return data; } function _legacy() { const data = require("@teambit/legacy.extension-data"); _legacy = function () { return data; }; return data; } function _lodash() { const data = require("lodash"); _lodash = function () { return data; }; return data; } function _mergeConfigConflict() { const data = require("./exceptions/merge-config-conflict"); _mergeConfigConflict = function () { return data; }; return data; } function _workspace() { const data = require("./workspace"); _workspace = function () { return data; }; return data; } function _mergeConflictFile() { const data = require("./merge-conflict-file"); _mergeConflictFile = function () { return data; }; return data; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } class AspectsMerger { constructor(workspace, harmony) { this.workspace = workspace; this.harmony = harmony; _defineProperty(this, "mergeConflictFile", void 0); _defineProperty(this, "mergeConfigDepsResolverDataCache", {}); this.mergeConflictFile = new (_mergeConflictFile().MergeConflictFile)(workspace.path); } getDepsDataOfMergeConfig(id) { return this.mergeConfigDepsResolverDataCache[id.toString()]; } /** * Calculate the component config based on: * the config property in the .bitmap file * the component.json file in the component folder * matching pattern in the variants config * defaults extensions from workspace config * extensions from the model. */ async merge(componentId, componentFromScope, excludeOrigins = []) { // TODO: consider caching this result let configFileExtensions; let variantsExtensions; const mergeFromScope = true; const errors = []; const bitMapEntry = this.workspace.consumer.bitMap.getComponentIfExist(componentId); const bitMapExtensions = bitMapEntry?.config; let configMerge; try { configMerge = this.mergeConflictFile.getConflictParsed(componentId.toStringWithoutVersion()); } catch (err) { if (!(err instanceof _mergeConfigConflict().MergeConfigConflict)) { throw err; } this.workspace.logger.error(`unable to parse the config file for ${componentId.toString()} due to conflicts`); errors.push(err); } const unmergedData = this.getUnmergedData(componentId); const unmergedDataMergeConf = unmergedData?.mergedConfig; const getMergeConfigCombined = () => { if (!configMerge && !unmergedDataMergeConf) return undefined; if (!configMerge) return unmergedDataMergeConf; if (!unmergedDataMergeConf) return configMerge; return (0, _lodash().mergeWith)(configMerge, unmergedDataMergeConf, (objValue, srcValue) => { if (Array.isArray(objValue)) { // critical for dependencyResolver.policy.*dependencies. otherwise, it will override the array return objValue.concat(srcValue); } return undefined; }); }; const mergeConfigCombined = getMergeConfigCombined(); _legacy().ExtensionDataList.adjustEnvsOnConfigObject(mergeConfigCombined || {}); const configMergeExtensions = mergeConfigCombined ? _legacy().ExtensionDataList.fromConfigObject(mergeConfigCombined) : undefined; this.removeAutoDepsFromConfig(componentId, configMergeExtensions); const scopeExtensionsBeforeClone = this.getComponentFromScopeWithoutDuplications(componentFromScope); const scopeExtensions = _legacy().ExtensionDataList.fromArray(scopeExtensionsBeforeClone.map(e => e.clone())); // backward compatibility. previously, it was saved as an array into the model (when there was merge-config) this.removeAutoDepsFromConfig(componentId, scopeExtensions, true); const [specific, nonSpecific] = (0, _lodash().partition)(scopeExtensions, entry => entry.config[_workspace().AspectSpecificField] === true); const scopeExtensionsNonSpecific = new (_legacy().ExtensionDataList)(...nonSpecific); const scopeExtensionsSpecific = new (_legacy().ExtensionDataList)(...specific); this.addConfigDepsFromModelToConfigMerge(scopeExtensionsSpecific, mergeConfigCombined); const componentConfigFile = await this.workspace.componentConfigFile(componentId); if (componentConfigFile) { configFileExtensions = componentConfigFile.aspects.toLegacy(); } const relativeComponentDir = this.workspace.componentDir(componentId, { ignoreVersion: true }, { relative: true }); const variantConfig = this.workspace.variants.byRootDirAndName(relativeComponentDir, componentId.fullName); if (variantConfig) { variantsExtensions = variantConfig.extensions.clone(); // Do not merge from scope when there is specific variant (which is not *) that match the component // if (variantConfig.maxSpecificity > 0) { // mergeFromScope = false; // } } // We don't stop on each step because we want to merge the default scope even if propagate=false but the default scope is not defined // in the case the same extension pushed twice, the former takes precedence (opposite of Object.assign) const extensionsToMerge = []; let envWasFoundPreviously = false; const removedExtensionIds = []; const addExtensionsToMerge = async (extensions, origin, extraData) => { if (!extensions.length) { return; } removedExtensionIds.push(...extensions.filter(extData => extData.isRemoved).map(extData => extData.stringId)); const extsWithoutRemoved = extensions.filterRemovedExtensions(); const selfInMergedExtensions = extsWithoutRemoved.findExtension(componentId.toStringWithoutVersion(), true); const extsWithoutSelf = selfInMergedExtensions?.extensionId ? extsWithoutRemoved.remove(selfInMergedExtensions.extensionId) : extsWithoutRemoved; const preferWorkspaceVersion = origin !== 'BitmapFile' && origin !== 'ComponentJsonFile'; // it's important to do this resolution before the merge, otherwise we have issues with extensions // coming from scope with local scope name, as opposed to the same extension comes from the workspace with default scope name // also, it's important to do it before filtering the env, because when the env was not exported, it's saved with scope-name // inside the "env" prop of teambit.envs/env, but without scope-name in the root. const extsWithUpdatedIds = await this.resolveExtensionListIds(extsWithoutSelf, preferWorkspaceVersion); const { extensionDataListFiltered, envIsCurrentlySet } = await this.filterEnvsFromExtensionsIfNeeded(extsWithUpdatedIds, envWasFoundPreviously, origin); if (envIsCurrentlySet) { envWasFoundPreviously = true; } extensionsToMerge.push({ origin, extensions: extensionDataListFiltered, extraData }); }; const setDataListAsSpecific = extensions => { extensions.forEach(dataEntry => dataEntry.config[_workspace().AspectSpecificField] = true); }; if (bitMapExtensions && !excludeOrigins.includes('BitmapFile')) { const extensionDataList = _legacy().ExtensionDataList.fromConfigObject(bitMapExtensions); setDataListAsSpecific(extensionDataList); await addExtensionsToMerge(extensionDataList, 'BitmapFile'); } // config-merge is after the .bitmap. because normally if you have config set in .bitmap, you won't // be able to lane-merge. (unless you specify --ignore-config-changes). // so, if there is config in .bitmap, it probably happened after lane-merge. if (configMergeExtensions && !excludeOrigins.includes('ConfigMerge')) { await addExtensionsToMerge(_legacy().ExtensionDataList.fromArray(configMergeExtensions), 'ConfigMerge'); } if (configFileExtensions && !excludeOrigins.includes('ComponentJsonFile')) { setDataListAsSpecific(configFileExtensions); await addExtensionsToMerge(configFileExtensions, 'ComponentJsonFile'); } if (!excludeOrigins.includes('ModelSpecific')) { await addExtensionsToMerge(_legacy().ExtensionDataList.fromArray(scopeExtensionsSpecific), 'ModelSpecific'); } let continuePropagating = componentConfigFile?.propagate ?? true; if (variantsExtensions && continuePropagating && !excludeOrigins.includes('WorkspaceVariants')) { const appliedRules = variantConfig?.sortedMatches.map(({ pattern, specificity }) => ({ pattern, specificity })); await addExtensionsToMerge(variantsExtensions, 'WorkspaceVariants', { appliedRules }); } continuePropagating = continuePropagating && (variantConfig?.propagate ?? true); if (mergeFromScope && continuePropagating && !excludeOrigins.includes('ModelNonSpecific')) { await addExtensionsToMerge(scopeExtensionsNonSpecific, 'ModelNonSpecific'); } const afterMerge = _legacy().ExtensionDataList.mergeConfigs(extensionsToMerge.map(ext => ext.extensions)); const withoutRemoved = afterMerge.filter(extData => !removedExtensionIds.includes(extData.stringId)); // clone the extension data to avoid mutating the original data (specifically, we don't want to mutate the scope data) const extensions = _legacy().ExtensionDataList.fromArray(withoutRemoved); return { extensions, beforeMerge: extensionsToMerge, errors }; } /** * before version 0.0.882 it was possible to save Version object with the same extension twice. */ getComponentFromScopeWithoutDuplications(componentFromScope) { if (!componentFromScope) return new (_legacy().ExtensionDataList)(); const scopeExtensions = componentFromScope.config.extensions; const scopeExtIds = scopeExtensions.ids; const scopeExtHasDuplications = scopeExtIds.length !== (0, _lodash().uniq)(scopeExtIds).length; if (!scopeExtHasDuplications) { return scopeExtensions; } // let's remove this duplicated extension blindly without trying to merge. (no need to merge coz it's old data from scope // which will be overridden anyway by the workspace or other config strategies). const arr = (0, _lodash().uniqWith)(scopeExtensions, (0, _legacy().getCompareExtPredicate)(true)); return _legacy().ExtensionDataList.fromArray(arr); } /** * from the merge-config we get the dep-resolver policy as an array, because it needs to support "force" prop. * however, when we save the config, we want to save it as an object, so we need split the data into two: * 1. force: true, which gets saved into the config. * 2. force: false, which gets saved into the data.dependencies later on. see the workspace.getAutoDetectOverrides() */ removeAutoDepsFromConfig(componentId, conf, fromScope = false) { if (!conf) return; const autoDepsObj = conf.extractAutoDepsFromConfig(); if (!autoDepsObj) return; if (!fromScope) { if (!this.mergeConfigDepsResolverDataCache[componentId.toString()]) { this.mergeConfigDepsResolverDataCache[componentId.toString()] = {}; } this.mergeConfigDepsResolverDataCache[componentId.toString()] = (0, _lodash().merge)(this.mergeConfigDepsResolverDataCache[componentId.toString()], autoDepsObj); } } /** * this is needed because if the mergeConfig has a policy, it will be used, and any other policy along the line will be ignored. * in case the model has some dependencies that were set explicitly they're gonna be ignored. * this makes sure to add them to the policy of the mergeConfig. * in a way, this is similar to what we do when a user is running `bit deps set` and the component had previous dependencies set, * we copy those dependencies along with the current one to the .bitmap file, so they won't get lost. */ addConfigDepsFromModelToConfigMerge(scopeExtensionsSpecific, mergeConfig) { const mergeConfigPolicy = mergeConfig?.[_dependencyResolver().DependencyResolverAspect.id]?.policy; if (!mergeConfigPolicy) return; const scopePolicy = scopeExtensionsSpecific.findCoreExtension(_dependencyResolver().DependencyResolverAspect.id)?.config.policy; if (!scopePolicy) return; Object.keys(scopePolicy).forEach(key => { if (!mergeConfigPolicy[key]) { mergeConfigPolicy[key] = scopePolicy[key]; return; } // mergeConfigPolicy should take precedence over scopePolicy mergeConfigPolicy[key] = _objectSpread(_objectSpread({}, scopePolicy[key]), mergeConfigPolicy[key]); }); } getUnmergedData(componentId) { return this.workspace.scope.legacyScope.objects.unmergedComponents.getEntry(componentId); } async filterEnvsFromExtensionsIfNeeded(extensionDataList, envWasFoundPreviously, origin) { const envAspect = extensionDataList.findExtension(_envs().EnvsAspect.id); const envFromEnvsAspect = envAspect?.config.env || envAspect?.data.id; const aspectsRegisteredAsEnvs = extensionDataList.filter(aspect => this.workspace.envs.getEnvDefinitionByStringId(aspect.newExtensionId?.toString() || aspect.stringId)).map(aspect => aspect.stringId); if (envWasFoundPreviously && (envAspect || aspectsRegisteredAsEnvs.length)) { const nonEnvs = extensionDataList.map(e => { // normally the env-id inside the envs aspect doesn't have a version, but the aspect itself has a version. // also, the env-id inside the envs aspect includes the default-scope, but the aspect itself doesn't. if (envFromEnvsAspect && e.stringId === envFromEnvsAspect || envFromEnvsAspect && e.extensionId?.toStringWithoutVersion() === envFromEnvsAspect || aspectsRegisteredAsEnvs.includes(e.stringId)) { return undefined; } if (e.stringId === envAspect?.stringId) { // must clone the env aspect to avoid mutating the original data const clonedEnvAspect = e.clone(); delete clonedEnvAspect.config.env; // aspect env may have other data other then config.env. return clonedEnvAspect; } return e; }); return { extensionDataListFiltered: new (_legacy().ExtensionDataList)(...(0, _lodash().compact)(nonEnvs)), envIsCurrentlySet: true }; } if (envFromEnvsAspect && (origin === 'ModelNonSpecific' || origin === 'ModelSpecific')) { // if env was found, search for this env in the workspace and if found, replace the env-id with the one from the workspace const envAspectExt = extensionDataList.find(e => e.extensionId?.toStringWithoutVersion() === envFromEnvsAspect); const ids = this.workspace.listIds(); const envAspectId = envAspectExt?.extensionId; const found = envAspectId && ids.find(id => id.isEqualWithoutVersion(envAspectId)); if (found) { envAspectExt.extensionId = found; } } return { extensionDataListFiltered: extensionDataList, envIsCurrentlySet: Boolean(envFromEnvsAspect) }; } /** * This will mutate the entries with extensionId prop to have resolved legacy id * This should be worked on the extension data list not the new aspect list * @param extensionList */ async resolveExtensionListIds(extensionList, preferWorkspaceVersion = false) { const promises = extensionList.map(async entry => { if (entry.extensionId) { // don't pass `entry.extensionId` (as ComponentID) to `resolveComponentId` because then it'll use the optimization // of parsing it to ComponentID without checking the workspace. Normally, this optimization is good, but here // in case the extension wasn't exported, the ComponentID is wrong, it has the scope-name due to incorrect ComponentID.fromString // in configEntryToDataEntry() function. It'd be ideal to fix it from there but it's not easy. const componentId = await this.workspace.resolveComponentId(entry.extensionId.toString()); const idFromWorkspace = preferWorkspaceVersion ? this.workspace.getIdIfExist(componentId) : undefined; const id = idFromWorkspace || componentId; entry.extensionId = id; entry.newExtensionId = id; } return entry; }); await Promise.all(promises); return extensionList; } } exports.AspectsMerger = AspectsMerger; //# sourceMappingURL=aspects-merger.js.map