UNPKG

@rushstack/heft-config-file

Version:

Configuration file loader for @rushstack/heft

627 lines 34.5 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import * as nodeJsPath from 'node:path'; import { JSONPath } from 'jsonpath-plus'; import { JsonSchema, JsonFile, Import, FileSystem } from '@rushstack/node-core-library'; /* eslint-disable @typescript-eslint/typedef,@typescript-eslint/no-redeclare,@typescript-eslint/no-namespace,@typescript-eslint/naming-convention */ // This structure is used so that consumers can pass raw string literals and have it typecheck, without breaking existing callers. /** * @beta * * The set of possible mechanisms for merging properties from parent configuration files. * If a child configuration file sets a property value to `null`, that will always delete the value * specified in the parent configuration file, regardless of the inheritance type. */ const InheritanceType = { /** * Append additional elements after elements from the parent file's property. Only applicable * for arrays. */ append: 'append', /** * Perform a shallow merge of additional elements after elements from the parent file's property. * Only applicable for objects. */ merge: 'merge', /** * Discard elements from the parent file's property */ replace: 'replace', /** * Custom inheritance functionality */ custom: 'custom' }; export { InheritanceType }; /** * @beta * * The set of possible resolution methods for fields that refer to paths. */ const PathResolutionMethod = { /** * Resolve a path relative to the configuration file */ resolvePathRelativeToConfigurationFile: 'resolvePathRelativeToConfigurationFile', /** * Resolve a path relative to the root of the project containing the configuration file */ resolvePathRelativeToProjectRoot: 'resolvePathRelativeToProjectRoot', /** * Treat the property as a NodeJS-style require/import reference and resolve using standard * NodeJS filesystem resolution * * @deprecated * Use {@link (PathResolutionMethod:variable).nodeResolve} instead */ NodeResolve: 'NodeResolve', /** * Treat the property as a NodeJS-style require/import reference and resolve using standard * NodeJS filesystem resolution */ nodeResolve: 'nodeResolve', /** * Resolve the property using a custom resolver. */ custom: 'custom' }; export { PathResolutionMethod }; /* eslint-enable @typescript-eslint/typedef,@typescript-eslint/no-redeclare,@typescript-eslint/no-namespace,@typescript-eslint/naming-convention */ const CONFIGURATION_FILE_MERGE_BEHAVIOR_FIELD_REGEX = /^\$([^\.]+)\.inheritanceType$/; export const CONFIGURATION_FILE_FIELD_ANNOTATION = Symbol('configuration-file-field-annotation'); /** * @beta */ export class ConfigurationFileBase { get _schema() { if (!this.__schema) { this.__schema = this._getSchema(); } return this.__schema; } constructor(options) { this._configCache = new Map(); this._configPromiseCache = new Map(); const { jsonSchemaObject, jsonSchemaPath, jsonPathMetadata = {}, propertyInheritance = {}, propertyInheritanceDefaults = {}, customValidationFunction } = options; if (jsonSchemaObject) { this._getSchema = () => JsonSchema.fromLoadedObject(jsonSchemaObject); } else { this._getSchema = () => JsonSchema.fromFile(jsonSchemaPath); } this._jsonPathMetadata = Object.entries(jsonPathMetadata); this._propertyInheritanceTypes = propertyInheritance; this._defaultPropertyInheritance = propertyInheritanceDefaults; this._customValidationFunction = customValidationFunction; } /** * Get the path to the source file that the referenced property was originally * loaded from. */ getObjectSourceFilePath(obj) { const { [CONFIGURATION_FILE_FIELD_ANNOTATION]: annotation } = obj; return annotation === null || annotation === void 0 ? void 0 : annotation.configurationFilePath; } /** * Get the value of the specified property on the specified object that was originally * loaded from a configuration file. */ getPropertyOriginalValue(options) { const { parentObject, propertyName } = options; const { [CONFIGURATION_FILE_FIELD_ANNOTATION]: annotation } = parentObject; if (annotation === null || annotation === void 0 ? void 0 : annotation.originalValues.hasOwnProperty(propertyName)) { return annotation.originalValues[propertyName]; } } /** * Get the original value of the `$schema` property from the original configuration file, it one was present. */ getSchemaPropertyOriginalValue(obj) { const { [CONFIGURATION_FILE_FIELD_ANNOTATION]: annotation } = obj; return annotation === null || annotation === void 0 ? void 0 : annotation.schemaPropertyOriginalValue; } _loadConfigurationFileInnerWithCache(terminal, resolvedConfigurationFilePath, projectFolderPath, onConfigurationFileNotFound) { const visitedConfigurationFilePaths = new Set(); const cacheEntry = this._loadConfigurationFileEntryWithCache(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, onConfigurationFileNotFound); const result = this._finalizeConfigurationFile(cacheEntry, projectFolderPath, terminal); return result; } async _loadConfigurationFileInnerWithCacheAsync(terminal, resolvedConfigurationFilePath, projectFolderPath, onFileNotFound) { const visitedConfigurationFilePaths = new Set(); const cacheEntry = await this._loadConfigurationFileEntryWithCacheAsync(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, onFileNotFound); const result = this._finalizeConfigurationFile(cacheEntry, projectFolderPath, terminal); return result; } _loadConfigurationFileEntryWithCache(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, onFileNotFound) { if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) { const resolvedConfigurationFilePathForLogging = ConfigurationFileBase._formatPathForLogging(resolvedConfigurationFilePath); throw new Error('A loop has been detected in the "extends" properties of configuration file at ' + `"${resolvedConfigurationFilePathForLogging}".`); } visitedConfigurationFilePaths.add(resolvedConfigurationFilePath); let cacheEntry = this._configCache.get(resolvedConfigurationFilePath); if (!cacheEntry) { cacheEntry = this._loadConfigurationFileEntry(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, onFileNotFound); this._configCache.set(resolvedConfigurationFilePath, cacheEntry); } return cacheEntry; } async _loadConfigurationFileEntryWithCacheAsync(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, onConfigurationFileNotFound) { if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) { const resolvedConfigurationFilePathForLogging = ConfigurationFileBase._formatPathForLogging(resolvedConfigurationFilePath); throw new Error('A loop has been detected in the "extends" properties of configuration file at ' + `"${resolvedConfigurationFilePathForLogging}".`); } visitedConfigurationFilePaths.add(resolvedConfigurationFilePath); let cacheEntryPromise = this._configPromiseCache.get(resolvedConfigurationFilePath); if (!cacheEntryPromise) { cacheEntryPromise = this._loadConfigurationFileEntryAsync(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, onConfigurationFileNotFound).then((value) => { this._configCache.set(resolvedConfigurationFilePath, value); return value; }); this._configPromiseCache.set(resolvedConfigurationFilePath, cacheEntryPromise); } return await cacheEntryPromise; } /** * Parses the raw JSON-with-comments text of the configuration file. * @param fileText - The text of the configuration file * @param resolvedConfigurationFilePathForLogging - The path to the configuration file, formatted for logs * @returns The parsed configuration file */ _parseConfigurationFile(fileText, resolvedConfigurationFilePathForLogging) { let configurationJson; try { configurationJson = JsonFile.parseString(fileText); } catch (e) { throw new Error(`In configuration file "${resolvedConfigurationFilePathForLogging}": ${e}`); } return configurationJson; } /** * Resolves all path properties and annotates properties with their original values. * @param entry - The cache entry for the loaded configuration file * @param projectFolderPath - The project folder path, if applicable * @returns The configuration file with all path properties resolved */ _contextualizeConfigurationFile(entry, projectFolderPath) { // Deep copy the configuration file because different callers might contextualize properties differently. const result = structuredClone(entry.configurationFile); const { resolvedConfigurationFilePath } = entry; this._annotateProperties(resolvedConfigurationFilePath, result); for (const [jsonPath, metadata] of this._jsonPathMetadata) { JSONPath({ path: jsonPath, json: result, callback: (payload, payloadType, fullPayload) => { const resolvedPath = this._resolvePathProperty({ propertyName: fullPayload.path, propertyValue: fullPayload.value, configurationFilePath: resolvedConfigurationFilePath, configurationFile: result, projectFolderPath }, metadata); fullPayload.parent[fullPayload.parentProperty] = resolvedPath; }, otherTypeCallback: () => { throw new Error('@other() tags are not supported'); } }); } return result; } /** * Resolves all path properties and merges parent properties. * @param entry - The cache entry for the loaded configuration file * @param projectFolderPath - The project folder path, if applicable * @returns The flattened, unvalidated configuration file, with path properties resolved */ _contextualizeAndFlattenConfigurationFile(entry, projectFolderPath) { const { parent, resolvedConfigurationFilePath } = entry; const parentConfig = parent ? this._contextualizeAndFlattenConfigurationFile(parent, projectFolderPath) : {}; const currentConfig = this._contextualizeConfigurationFile(entry, projectFolderPath); const result = this._mergeConfigurationFiles(parentConfig, currentConfig, resolvedConfigurationFilePath); return result; } /** * Resolves all path properties, merges parent properties, and validates the configuration file. * @param entry - The cache entry for the loaded configuration file * @param projectFolderPath - The project folder path, if applicable * @param terminal - The terminal to log validation messages to * @returns The finalized configuration file */ _finalizeConfigurationFile(entry, projectFolderPath, terminal) { const { resolvedConfigurationFilePathForLogging } = entry; const result = this._contextualizeAndFlattenConfigurationFile(entry, projectFolderPath); try { this._schema.validateObject(result, resolvedConfigurationFilePathForLogging); } catch (e) { throw new Error(`Resolved configuration object does not match schema: ${e}`); } if (this._customValidationFunction && !this._customValidationFunction(result, resolvedConfigurationFilePathForLogging, terminal)) { // To suppress this error, the function may throw its own error, such as an AlreadyReportedError if it already // logged to the terminal. throw new Error(`Resolved configuration file at "${resolvedConfigurationFilePathForLogging}" failed custom validation.`); } // If the schema validates, we can assume that the configuration file is complete. return result; } // NOTE: Internal calls to load a configuration file should use `_loadConfigurationFileInnerWithCache`. // Don't call this function directly, as it does not provide config file loop detection, // and you won't get the advantage of queueing up for a config file that is already loading. _loadConfigurationFileEntry(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, fileNotFoundFallback) { const resolvedConfigurationFilePathForLogging = ConfigurationFileBase._formatPathForLogging(resolvedConfigurationFilePath); let fileText; try { fileText = FileSystem.readFile(resolvedConfigurationFilePath); } catch (e) { if (FileSystem.isNotExistError(e)) { const fallbackPath = fileNotFoundFallback === null || fileNotFoundFallback === void 0 ? void 0 : fileNotFoundFallback(resolvedConfigurationFilePathForLogging); if (fallbackPath) { try { return this._loadConfigurationFileEntryWithCache(terminal, fallbackPath, visitedConfigurationFilePaths); } catch (fallbackError) { if (!FileSystem.isNotExistError(fallbackError)) { throw fallbackError; } // Otherwise report the missing original file. } } terminal.writeDebugLine(`Configuration file "${resolvedConfigurationFilePathForLogging}" not found.`); e.message = `File does not exist: ${resolvedConfigurationFilePathForLogging}`; } throw e; } const configurationJson = this._parseConfigurationFile(fileText, resolvedConfigurationFilePathForLogging); let parentConfiguration; if (configurationJson.extends) { try { const resolvedParentConfigPath = Import.resolveModule({ modulePath: configurationJson.extends, baseFolderPath: nodeJsPath.dirname(resolvedConfigurationFilePath) }); parentConfiguration = this._loadConfigurationFileEntryWithCache(terminal, resolvedParentConfigPath, visitedConfigurationFilePaths); } catch (e) { if (FileSystem.isNotExistError(e)) { throw new Error(`In file "${resolvedConfigurationFilePathForLogging}", file referenced in "extends" property ` + `("${configurationJson.extends}") cannot be resolved.`); } else { throw e; } } } const result = { configurationFile: configurationJson, resolvedConfigurationFilePath, resolvedConfigurationFilePathForLogging, parent: parentConfiguration }; return result; } // NOTE: Internal calls to load a configuration file should use `_loadConfigurationFileInnerWithCacheAsync`. // Don't call this function directly, as it does not provide config file loop detection, // and you won't get the advantage of queueing up for a config file that is already loading. async _loadConfigurationFileEntryAsync(terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, fileNotFoundFallback) { const resolvedConfigurationFilePathForLogging = ConfigurationFileBase._formatPathForLogging(resolvedConfigurationFilePath); let fileText; try { fileText = await FileSystem.readFileAsync(resolvedConfigurationFilePath); } catch (e) { if (FileSystem.isNotExistError(e)) { const fallbackPath = fileNotFoundFallback === null || fileNotFoundFallback === void 0 ? void 0 : fileNotFoundFallback(resolvedConfigurationFilePathForLogging); if (fallbackPath) { try { return await this._loadConfigurationFileEntryWithCacheAsync(terminal, fallbackPath, visitedConfigurationFilePaths); } catch (fallbackError) { if (!FileSystem.isNotExistError(fallbackError)) { throw fallbackError; } // Otherwise report the missing original file. } } terminal.writeDebugLine(`Configuration file "${resolvedConfigurationFilePathForLogging}" not found.`); e.message = `File does not exist: ${resolvedConfigurationFilePathForLogging}`; } throw e; } const configurationJson = this._parseConfigurationFile(fileText, resolvedConfigurationFilePathForLogging); let parentConfiguration; if (configurationJson.extends) { try { const resolvedParentConfigPath = await Import.resolveModuleAsync({ modulePath: configurationJson.extends, baseFolderPath: nodeJsPath.dirname(resolvedConfigurationFilePath) }); parentConfiguration = await this._loadConfigurationFileEntryWithCacheAsync(terminal, resolvedParentConfigPath, visitedConfigurationFilePaths); } catch (e) { if (FileSystem.isNotExistError(e)) { throw new Error(`In file "${resolvedConfigurationFilePathForLogging}", file referenced in "extends" property ` + `("${configurationJson.extends}") cannot be resolved.`); } else { throw e; } } } const result = { configurationFile: configurationJson, resolvedConfigurationFilePath, resolvedConfigurationFilePathForLogging, parent: parentConfiguration }; return result; } _annotateProperties(resolvedConfigurationFilePath, root) { if (!root) { return; } const queue = new Set([root]); for (const obj of queue) { if (obj && typeof obj === 'object') { obj[CONFIGURATION_FILE_FIELD_ANNOTATION] = { configurationFilePath: resolvedConfigurationFilePath, originalValues: { ...obj } }; for (const objValue of Object.values(obj)) { queue.add(objValue); } } } } _resolvePathProperty(resolverOptions, metadata) { const { propertyValue, configurationFilePath, projectFolderPath } = resolverOptions; const resolutionMethod = metadata.pathResolutionMethod; if (resolutionMethod === undefined) { return propertyValue; } switch (metadata.pathResolutionMethod) { case PathResolutionMethod.resolvePathRelativeToConfigurationFile: { return nodeJsPath.resolve(nodeJsPath.dirname(configurationFilePath), propertyValue); } case PathResolutionMethod.resolvePathRelativeToProjectRoot: { const packageRoot = projectFolderPath; if (!packageRoot) { throw new Error(`Project-relative resolution was requested in "${ConfigurationFileBase._formatPathForLogging(configurationFilePath)}" but no project root was provided to the configuration file loader.`); } return nodeJsPath.resolve(packageRoot, propertyValue); } case PathResolutionMethod.NodeResolve: // TODO: Remove case PathResolutionMethod.nodeResolve: { return Import.resolveModule({ modulePath: propertyValue, baseFolderPath: nodeJsPath.dirname(configurationFilePath) }); } case PathResolutionMethod.custom: { if (!metadata.customResolver) { throw new Error(`The pathResolutionMethod was set to "${PathResolutionMethod[resolutionMethod]}", but a custom ` + 'resolver was not provided.'); } return metadata.customResolver(resolverOptions); } default: { throw new Error(`Unsupported PathResolutionMethod: ${PathResolutionMethod[resolutionMethod]} (${resolutionMethod})`); } } } _mergeConfigurationFiles(parentConfiguration, configurationJson, resolvedConfigurationFilePath) { const ignoreProperties = new Set(['extends', '$schema']); // Need to do a dance with the casting here because while we know that JSON keys are always // strings, TypeScript doesn't. const result = this._mergeObjects(parentConfiguration, configurationJson, resolvedConfigurationFilePath, this._defaultPropertyInheritance, this._propertyInheritanceTypes, ignoreProperties); const schemaPropertyOriginalValue = configurationJson.$schema; result[CONFIGURATION_FILE_FIELD_ANNOTATION].schemaPropertyOriginalValue = schemaPropertyOriginalValue; return result; } _mergeObjects(parentObject, currentObject, resolvedConfigurationFilePath, defaultPropertyInheritance, configuredPropertyInheritance, ignoreProperties) { var _a, _b, _c; const resultAnnotation = { configurationFilePath: resolvedConfigurationFilePath, originalValues: {} }; const result = { [CONFIGURATION_FILE_FIELD_ANNOTATION]: resultAnnotation }; // An array of property names that are on the merging object. Typed as Set<string> since it may // contain inheritance type annotation keys, or other built-in properties that we ignore // (eg. "extends", "$schema"). const currentObjectPropertyNames = new Set(Object.keys(currentObject)); // A map of property names to their inheritance type. const inheritanceTypeMap = new Map(); // The set of property names that should be included in the resulting object // All names from the parent are assumed to already be filtered. const mergedPropertyNames = new Set(Object.keys(parentObject)); // Do a first pass to gather and strip the inheritance type annotations from the merging object. for (const propertyName of currentObjectPropertyNames) { if (ignoreProperties && ignoreProperties.has(propertyName)) { continue; } // Try to get the inheritance type annotation from the merging object using the regex. // Note: since this regex matches a specific style of property name, we should not need to // allow for any escaping of $-prefixed properties. If this ever changes (eg. to allow for // `"$propertyName": { ... }` options), then we'll likely need to handle that error case, // as well as allow escaping $-prefixed properties that developers want to be serialized, // possibly by using the form `$$propertyName` to escape `$propertyName`. const inheritanceTypeMatches = propertyName.match(CONFIGURATION_FILE_MERGE_BEHAVIOR_FIELD_REGEX); if (inheritanceTypeMatches) { // Should always be of length 2, since the first match is the entire string and the second // match is the capture group. const mergeTargetPropertyName = inheritanceTypeMatches[1]; const inheritanceTypeRaw = currentObject[propertyName]; if (!currentObjectPropertyNames.has(mergeTargetPropertyName)) { throw new Error(`Issue in processing configuration file property "${propertyName}". ` + `An inheritance type was provided but no matching property was found in the parent.`); } else if (typeof inheritanceTypeRaw !== 'string') { throw new Error(`Issue in processing configuration file property "${propertyName}". ` + `An unsupported inheritance type was provided: ${JSON.stringify(inheritanceTypeRaw)}`); } else if (typeof currentObject[mergeTargetPropertyName] !== 'object') { throw new Error(`Issue in processing configuration file property "${propertyName}". ` + `An inheritance type was provided for a property that is not a keyed object or array.`); } switch (inheritanceTypeRaw.toLowerCase()) { case 'append': inheritanceTypeMap.set(mergeTargetPropertyName, { inheritanceType: InheritanceType.append }); break; case 'merge': inheritanceTypeMap.set(mergeTargetPropertyName, { inheritanceType: InheritanceType.merge }); break; case 'replace': inheritanceTypeMap.set(mergeTargetPropertyName, { inheritanceType: InheritanceType.replace }); break; default: throw new Error(`Issue in processing configuration file property "${propertyName}". ` + `An unsupported inheritance type was provided: "${inheritanceTypeRaw}"`); } } else { mergedPropertyNames.add(propertyName); } } // Cycle through properties and merge them for (const propertyName of mergedPropertyNames) { const propertyValue = currentObject[propertyName]; const parentPropertyValue = parentObject[propertyName]; let newValue; const usePropertyValue = () => { resultAnnotation.originalValues[propertyName] = this.getPropertyOriginalValue({ parentObject: currentObject, propertyName: propertyName }); newValue = propertyValue; }; const useParentPropertyValue = () => { resultAnnotation.originalValues[propertyName] = this.getPropertyOriginalValue({ parentObject: parentObject, propertyName: propertyName }); newValue = parentPropertyValue; }; if (propertyValue === null) { if (parentPropertyValue !== undefined) { resultAnnotation.originalValues[propertyName] = this.getPropertyOriginalValue({ parentObject: parentObject, propertyName: propertyName }); } newValue = undefined; } else if (propertyValue !== undefined && parentPropertyValue === undefined) { usePropertyValue(); } else if (parentPropertyValue !== undefined && propertyValue === undefined) { useParentPropertyValue(); } else if (propertyValue !== undefined && parentPropertyValue !== undefined) { // If the property is an inheritance type annotation, use it, otherwise fallback to the configured // top-level property inheritance, if one is specified. let propertyInheritance = (_a = inheritanceTypeMap.get(propertyName)) !== null && _a !== void 0 ? _a : configuredPropertyInheritance === null || configuredPropertyInheritance === void 0 ? void 0 : configuredPropertyInheritance[propertyName]; if (!propertyInheritance) { const bothAreArrays = Array.isArray(propertyValue) && Array.isArray(parentPropertyValue); if (bothAreArrays) { // If both are arrays, use the configured default array inheritance and fallback to appending // if one is not specified propertyInheritance = (_b = defaultPropertyInheritance.array) !== null && _b !== void 0 ? _b : { inheritanceType: InheritanceType.append }; } else { const bothAreObjects = propertyValue && parentPropertyValue && typeof propertyValue === 'object' && typeof parentPropertyValue === 'object'; if (bothAreObjects) { // If both are objects, use the configured default object inheritance and fallback to replacing // if one is not specified propertyInheritance = (_c = defaultPropertyInheritance.object) !== null && _c !== void 0 ? _c : { inheritanceType: InheritanceType.replace }; } else { // Fall back to replacing if they are of different types, since we don't know how to merge these propertyInheritance = { inheritanceType: InheritanceType.replace }; } } } switch (propertyInheritance.inheritanceType) { case InheritanceType.replace: { usePropertyValue(); break; } case InheritanceType.append: { if (!Array.isArray(propertyValue) || !Array.isArray(parentPropertyValue)) { throw new Error(`Issue in processing configuration file property "${propertyName.toString()}". ` + `Property is not an array, but the inheritance type is set as "${InheritanceType.append}"`); } newValue = [...parentPropertyValue, ...propertyValue]; newValue[CONFIGURATION_FILE_FIELD_ANNOTATION] = { configurationFilePath: undefined, originalValues: { // eslint-disable-next-line @typescript-eslint/no-explicit-any ...parentPropertyValue[CONFIGURATION_FILE_FIELD_ANNOTATION].originalValues, // eslint-disable-next-line @typescript-eslint/no-explicit-any ...propertyValue[CONFIGURATION_FILE_FIELD_ANNOTATION].originalValues } }; break; } case InheritanceType.merge: { if (parentPropertyValue === null || propertyValue === null) { throw new Error(`Issue in processing configuration file property "${propertyName.toString()}". ` + `Null values cannot be used when the inheritance type is set as "${InheritanceType.merge}"`); } else if ((propertyValue && typeof propertyValue !== 'object') || (parentPropertyValue && typeof parentPropertyValue !== 'object')) { throw new Error(`Issue in processing configuration file property "${propertyName.toString()}". ` + `Primitive types cannot be provided when the inheritance type is set as "${InheritanceType.merge}"`); } else if (Array.isArray(propertyValue) || Array.isArray(parentPropertyValue)) { throw new Error(`Issue in processing configuration file property "${propertyName.toString()}". ` + `Property is not a keyed object, but the inheritance type is set as "${InheritanceType.merge}"`); } // Recursively merge the parent and child objects. Don't pass the configuredPropertyInheritance or // ignoreProperties because we are no longer at the top level of the configuration file. We also know // that it must be a string-keyed object, since the JSON spec requires it. newValue = this._mergeObjects(parentPropertyValue, propertyValue, resolvedConfigurationFilePath, defaultPropertyInheritance); break; } case InheritanceType.custom: { const customInheritance = propertyInheritance; if (!customInheritance.inheritanceFunction || typeof customInheritance.inheritanceFunction !== 'function') { throw new Error('For property inheritance type "InheritanceType.custom", an inheritanceFunction must be provided.'); } newValue = customInheritance.inheritanceFunction(propertyValue, parentPropertyValue); break; } default: { throw new Error(`Unknown inheritance type "${propertyInheritance}"`); } } } if (newValue !== undefined) { // Don't attach the key for undefined values so that they don't enumerate. result[propertyName] = newValue; } } return result; } } /** * @internal */ ConfigurationFileBase._formatPathForLogging = (path) => path; //# sourceMappingURL=ConfigurationFileBase.js.map