Skip to content
This repository has been archived by the owner on Jun 10, 2024. It is now read-only.

Commit

Permalink
feat: Report config name in error messages (#128)
Browse files Browse the repository at this point in the history
Co-authored-by: Francesco Trotta <github@fasttime.org>
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
3 people committed Apr 1, 2024
1 parent 26afaaa commit 58f8c9f
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 16 deletions.
99 changes: 89 additions & 10 deletions src/config-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,70 @@ const CONFIG_TYPES = new Set(['array', 'function']);

const FILES_AND_IGNORES_SCHEMA = new ObjectSchema(filesAndIgnoresSchema);

/**
* Wrapper error for config validation errors that adds a name to the front of the
* error message.
*/
class ConfigError extends Error {

/**
* Creates a new instance.
* @param {string} name The config object name causing the error.
* @param {number} index The index of the config object in the array.
* @param {Error} source The source error.
*/
constructor(name, index, source) {
super(`Config ${name}: ${source.message}`, { cause: source });

// copy over custom properties that aren't represented
for (const key of Object.keys(source)) {
if (!(key in this)) {
this[key] = source[key];
}
}

/**
* The name of the error.
* @type {string}
* @readonly
*/
this.name = 'ConfigError';

/**
* The index of the config object in the array.
* @type {number}
* @readonly
*/
this.index = index;
}
}

/**
* Gets the name of a config object.
* @param {object} config The config object to get the name of.
* @returns {string} The name of the config object.
*/
function getConfigName(config) {
if (typeof config.name === 'string' && config.name) {
return `"${config.name}"`;
}

return '(unnamed)';
}


/**
* Rethrows a config error with additional information about the config object.
* @param {object} config The config object to get the name of.
* @param {number} index The index of the config object in the array.
* @param {Error} error The error to rethrow.
* @throws {ConfigError} When the error is rethrown for a config.
*/
function rethrowConfigError(config, index, error) {
const configName = getConfigName(config);
throw new ConfigError(configName, index, error);
}

/**
* Shorthand for checking if a value is a string.
* @param {any} value The value to check.
Expand All @@ -43,23 +107,34 @@ function isString(value) {
}

/**
* Asserts that the files and ignores keys of a config object are valid as per base schema.
* @param {object} config The config object to check.
* Creates a function that asserts that the files and ignores keys
* of a config object are valid as per base schema.
* @param {Object} config The config object to check.
* @param {number} index The index of the config object in the array.
* @returns {void}
* @throws {TypeError} If the files and ignores keys of a config object are not valid.
* @throws {ConfigError} If the files and ignores keys of a config object are not valid.
*/
function assertValidFilesAndIgnores(config) {
function assertValidFilesAndIgnores(config, index) {

if (!config || typeof config !== 'object') {
return;
}

const validateConfig = { };

if ('files' in config) {
validateConfig.files = config.files;
}

if ('ignores' in config) {
validateConfig.ignores = config.ignores;
}
FILES_AND_IGNORES_SCHEMA.validate(validateConfig);

try {
FILES_AND_IGNORES_SCHEMA.validate(validateConfig);
} catch (validationError) {
rethrowConfigError(config, index, validationError);
}
}

/**
Expand Down Expand Up @@ -388,7 +463,7 @@ export class ConfigArray extends Array {
/**
* Tracks if the array has been normalized.
* @property isNormalized
* @type boolean
* @type {boolean}
* @private
*/
this[ConfigArraySymbol.isNormalized] = normalized;
Expand All @@ -407,7 +482,7 @@ export class ConfigArray extends Array {
* The path of the config file that this array was loaded from.
* This is used to calculate filename matches.
* @property basePath
* @type string
* @type {string}
*/
this.basePath = basePath;

Expand All @@ -416,14 +491,14 @@ export class ConfigArray extends Array {
/**
* The supported config types.
* @property configTypes
* @type Array<string>
* @type {Array<string>}
*/
this.extraConfigTypes = Object.freeze([...extraConfigTypes]);

/**
* A cache to store calculated configs for faster repeat lookup.
* @property configCache
* @type Map
* @type {Map<string, Object>}
* @private
*/
this[ConfigArraySymbol.configCache] = new Map();
Expand Down Expand Up @@ -809,7 +884,11 @@ export class ConfigArray extends Array {
// otherwise construct the config

finalConfig = matchingConfigIndices.reduce((result, index) => {
return this[ConfigArraySymbol.schema].merge(result, this[index]);
try {
return this[ConfigArraySymbol.schema].merge(result, this[index]);
} catch (validationError) {
rethrowConfigError(this[index], index, validationError);
}
}, {}, this);

finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig);
Expand Down
52 changes: 46 additions & 6 deletions tests/config-array.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,11 @@ describe('ConfigArray', () => {
title: 'should throw an error when files contains an invalid element',
configs: [
{
name: '',
files: ['*.js', undefined]
}
],
expectedError: 'Key "files": Items must be a string, a function, or an array of strings and functions.'
expectedError: 'Config (unnamed): Key "files": Items must be a string, a function, or an array of strings and functions.'
});

testValidationError({
Expand All @@ -382,28 +383,30 @@ describe('ConfigArray', () => {
ignores: undefined
}
],
expectedError: 'Key "ignores": Expected value to be an array.'
expectedError: 'Config (unnamed): Key "ignores": Expected value to be an array.'
});

testValidationError({
title: 'should throw an error when a global ignores contains an invalid element',
configs: [
{
name: 'foo',
ignores: ['ignored/**', -1]
}
],
expectedError: 'Key "ignores": Expected array to only contain strings and functions.'
expectedError: 'Config "foo": Key "ignores": Expected array to only contain strings and functions.'
});

testValidationError({
title: 'should throw an error when a non-global ignores contains an invalid element',
configs: [
{
name: 'foo',
files: ['*.js'],
ignores: [-1]
}
],
expectedError: 'Key "ignores": Expected array to only contain strings and functions.'
expectedError: 'Config "foo": Key "ignores": Expected array to only contain strings and functions.'
});

it('should throw an error when a config is not an object', async () => {
Expand All @@ -423,7 +426,7 @@ describe('ConfigArray', () => {

});

it('should throw an error when name is not a string', async () => {
it('should throw an error when base config name is not a string', async () => {
configs = new ConfigArray([
{
files: ['**'],
Expand All @@ -436,7 +439,25 @@ describe('ConfigArray', () => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Key "name": Property must be a string.');
.throw('Config (unnamed): Key "name": Property must be a string.');

});

it('should throw an error when additional config name is not a string', async () => {
configs = new ConfigArray([{}], { basePath });
configs.push(
{
files: ['**'],
name: true
}
);
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Config (unnamed): Key "name": Property must be a string.');

});
});
Expand Down Expand Up @@ -732,6 +753,25 @@ describe('ConfigArray', () => {
expect(config.defs.universal).to.be.true;
});

it('should throw an error when defs doesn\'t pass validation', async () => {
const configs = new ConfigArray([
{
files: ['**/*.js'],
defs: 'foo',
name: 'bar'
}
], { basePath, schema });

await configs.normalize();

const filename = path.resolve(basePath, 'foo.js');
expect(() => {
configs.getConfig(filename);
})
.to
.throw('Config "bar": Key "defs": Object expected.');
});

it('should calculate correct config when passed JS filename that matches a function config returning an array', () => {
const filename1 = path.resolve(basePath, 'baz.test.js');
const config1 = configs.getConfig(filename1);
Expand Down

0 comments on commit 58f8c9f

Please sign in to comment.