@teambit/workspace
Version:
348 lines (341 loc) • 17.9 kB
JavaScript
;
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