@percy/config 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ import { merge } from './utils/index.js';
2
+ import { getSchema } from './validate.js';
3
+ const {
4
+ isArray
5
+ } = Array;
6
+ const {
7
+ assign,
8
+ entries
9
+ } = Object; // Recursively walks a schema and collects defaults. When no schema is provided,
10
+ // the default config schema is used.
11
+
12
+ function getDefaultsFromSchema(schema) {
13
+ if (!schema || typeof schema.$ref === 'string') {
14
+ // get the schema from ajv
15
+ return getDefaultsFromSchema(getSchema((schema === null || schema === void 0 ? void 0 : schema.$ref) ?? '/config'));
16
+ } else if (schema.default != null) {
17
+ // return the default for this schema
18
+ return schema.default;
19
+ } else if (schema.type === 'object' && schema.properties) {
20
+ // return an object of default properties
21
+ return entries(schema.properties).reduce((acc, [prop, schema]) => {
22
+ let def = getDefaultsFromSchema(schema);
23
+ return def != null ? assign(acc || {}, {
24
+ [prop]: def
25
+ }) : acc;
26
+ }, undefined);
27
+ } else {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ export function getDefaults(overrides = {}) {
33
+ return merge([getDefaultsFromSchema(), overrides], (path, prev, next) => {
34
+ // override default array instead of merging
35
+ return isArray(next) && [path, next];
36
+ });
37
+ }
38
+ export default getDefaults;
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import load, { search } from './load.js';
2
+ import validate, { addSchema } from './validate.js';
3
+ import migrate, { addMigration } from './migrate.js';
4
+ import { merge, normalize, stringify } from './utils/index.js';
5
+ import getDefaults from './defaults.js'; // public config API
6
+
7
+ export { load, search, validate, addSchema, migrate, addMigration, getDefaults, merge, normalize, stringify }; // export the namespace by default
8
+
9
+ export * as default from './index.js';
package/dist/load.js ADDED
@@ -0,0 +1,100 @@
1
+ import fs from 'fs';
2
+ import { relative } from 'path';
3
+ import { cosmiconfigSync } from 'cosmiconfig';
4
+ import logger from '@percy/logger';
5
+ import migrate from './migrate.js';
6
+ import validate from './validate.js';
7
+ import getDefaults from './defaults.js';
8
+ import { inspect, normalize } from './utils/index.js'; // Loaded configuration file cache
9
+
10
+ export const cache = new Map(); // The cosmiconfig explorer used to load config files
11
+
12
+ export const explorer = cosmiconfigSync('percy', {
13
+ cache: false,
14
+ searchPlaces: ['package.json', '.percyrc', '.percy.json', '.percy.yaml', '.percy.yml', '.percy.js', 'percy.config.js']
15
+ }); // Searches within a provided directory, or loads the provided config path
16
+
17
+ export function search(path) {
18
+ try {
19
+ let result = path && !fs.statSync(path).isDirectory() ? explorer.load(path) : explorer.search(path);
20
+ return result || {};
21
+ } catch (error) {
22
+ if (error.code === 'ENOENT') return {};else throw error;
23
+ }
24
+ } // Finds and loads a config file using cosmiconfig, merges it with optional
25
+ // inputs, validates the combined config according to the schema, and returns
26
+ // the combined config. Loaded config files are cached and reused on next load,
27
+ // unless `reload` is true in which the file will be reloaded and the cache
28
+ // updated. Validation errors are logged as warnings and the config is returned
29
+ // unless `bail` is true. Supports kebab-case and camelCase config options and
30
+ // always returns camelCase options. Will automatically convert older config
31
+ // versions to the latest version while printing a warning.
32
+
33
+ export function load({
34
+ path,
35
+ overrides = {},
36
+ reload = false,
37
+ bail = false,
38
+ print = false
39
+ } = {}) {
40
+ var _Array$from;
41
+
42
+ // load cached config; when no path is specified, get the last config cached
43
+ let config = path ? cache.get(path) : (_Array$from = Array.from(cache)[cache.size - 1]) === null || _Array$from === void 0 ? void 0 : _Array$from[1];
44
+ let infoDebug = print ? 'info' : 'debug';
45
+ let errorDebug = print ? 'error' : 'debug';
46
+ let log = logger('config'); // load config or reload cached config
47
+
48
+ if (path !== false && (!config || reload)) {
49
+ try {
50
+ let result = search(path);
51
+
52
+ if (result !== null && result !== void 0 && result.config) {
53
+ log[infoDebug](`Found config file: ${relative('', result.filepath)}`);
54
+ let version = parseInt(result.config.version, 10);
55
+
56
+ if (Number.isNaN(version)) {
57
+ log.warn('Ignoring config file - missing or invalid version');
58
+ } else if (version > 2) {
59
+ log.warn(`Ignoring config file - unsupported version "${version}"`);
60
+ } else {
61
+ if (version < 2) {
62
+ log.warn('Found older config file version, please run ' + '`percy config:migrate` to update to the latest version');
63
+ }
64
+
65
+ config = migrate(result.config);
66
+ cache.set(path, config);
67
+ }
68
+ } else {
69
+ log[infoDebug]('Config file not found');
70
+ if (bail) return;
71
+ }
72
+ } catch (error) {
73
+ log[errorDebug](error);
74
+ if (bail) return;
75
+ }
76
+ } // normalize and merge with overrides then validate
77
+
78
+
79
+ config = normalize(config, {
80
+ overrides,
81
+ schema: '/config'
82
+ });
83
+ let errors = config && validate(config);
84
+
85
+ if (errors) {
86
+ log.warn('Invalid config:');
87
+
88
+ for (let e of errors) log.warn(`- ${e.path}: ${e.message}`);
89
+
90
+ if (bail) return;
91
+ }
92
+
93
+ if (path !== false && config) {
94
+ log[infoDebug](`Using config:\n${inspect(config)}`);
95
+ } // merge with defaults
96
+
97
+
98
+ return getDefaults(config);
99
+ }
100
+ export default load;
@@ -0,0 +1,77 @@
1
+ import logger from '@percy/logger';
2
+ import { get, set, del, map, joinPropertyPath, normalize } from './utils/index.js'; // Global set of registered migrations
3
+
4
+ const migrations = new Map(); // Register a migration function for the main config schema by default
5
+
6
+ export function addMigration(migration, schema = '/config') {
7
+ if (Array.isArray(migration)) {
8
+ return migration.map(m => addMigration(m, schema));
9
+ } else if (typeof migration !== 'function') {
10
+ return Object.entries(migration).map(([s, m]) => addMigration(m, s));
11
+ }
12
+
13
+ if (!migrations.has(schema)) migrations.set(schema, []);
14
+ migrations.get(schema).unshift(migration);
15
+ return migration;
16
+ } // Clear all migration functions
17
+
18
+ export function clearMigrations() {
19
+ migrations.clear();
20
+ addMigration(defaultMigration);
21
+ } // The default config migration
22
+
23
+ addMigration(defaultMigration);
24
+
25
+ function defaultMigration(config, {
26
+ set
27
+ }) {
28
+ if (config.version !== 2) set('version', 2);
29
+ } // Migrate util for deprecated options
30
+
31
+
32
+ function deprecate(config, log, path, options) {
33
+ if (get(config, path) == null) return;
34
+ let {
35
+ type,
36
+ until: ver,
37
+ map: mapto,
38
+ alt,
39
+ warn
40
+ } = options;
41
+ let name = joinPropertyPath(path);
42
+ let to = joinPropertyPath(mapto);
43
+ let message = 'The ' + [type ? `${type} option \`${name}\`` : `\`${name}\` option`, `will be removed in ${ver || 'a future release'}.`, to ? `Use \`${to}\` instead.` : alt || ''].join(' ').trim();
44
+ if (warn) log.warn(`Warning: ${message}`);else log.deprecated(message);
45
+ return to ? map(config, path, to) : config;
46
+ } // Calls each registered migration function with a normalize provided config
47
+ // and util functions for working with the config object
48
+
49
+
50
+ export function migrate(config, schema = '/config') {
51
+ config = normalize(config, {
52
+ schema
53
+ }) ?? {};
54
+
55
+ if (migrations.has(schema)) {
56
+ let log = logger('config');
57
+ let util = {
58
+ deprecate: deprecate.bind(null, config, log),
59
+ set: set.bind(null, config),
60
+ map: map.bind(null, config),
61
+ del: del.bind(null, config),
62
+ log
63
+ };
64
+
65
+ for (let migration of migrations.get(schema)) {
66
+ migration(config, util);
67
+ } // normalize again to adjust for migrations
68
+
69
+
70
+ config = normalize(config, {
71
+ schema
72
+ });
73
+ }
74
+
75
+ return config;
76
+ }
77
+ export default migrate;
@@ -0,0 +1,3 @@
1
+ export * from './merge.js';
2
+ export * from './normalize.js';
3
+ export * from './stringify.js';
@@ -0,0 +1,169 @@
1
+ const {
2
+ isArray
3
+ } = Array;
4
+ const {
5
+ isInteger
6
+ } = Number;
7
+ const {
8
+ entries
9
+ } = Object; // Creates an empty object or array
10
+
11
+ function create(array) {
12
+ return array ? [] : {};
13
+ } // Returns true or false if a subject has iterable keys or not
14
+
15
+
16
+ function hasKeys(subj) {
17
+ return isArray(subj) || Object.getPrototypeOf(subj) === Object.prototype;
18
+ } // Returns true if the provided key looks like an array key
19
+
20
+
21
+ const ARRAY_PATH_KEY_REG = /^(\[\d+]|0|[1-9]\d*)$/;
22
+ export function isArrayKey(key) {
23
+ return isInteger(key) || ARRAY_PATH_KEY_REG.test(key);
24
+ } // Split a property path string by dot or array notation
25
+
26
+ export function parsePropertyPath(path) {
27
+ return isArray(path) ? path : path.split('.').reduce((full, part) => {
28
+ return full.concat(part.split('[').reduce((f, p) => {
29
+ if (p.endsWith(']')) p = p.slice(0, -1);
30
+ return f.concat(isArrayKey(p) ? parseInt(p, 10) : p || []);
31
+ }, []));
32
+ }, []);
33
+ } // Join an array of path parts into a single path string
34
+
35
+ export function joinPropertyPath(path) {
36
+ path = !Array.isArray(path) ? path : path.filter(Boolean).map(k => isArrayKey(k) ? `[${k}]` : `.${k}`).join('');
37
+
38
+ while ((_path = path) !== null && _path !== void 0 && _path.startsWith('.')) {
39
+ var _path;
40
+
41
+ path = path.substr(1);
42
+ }
43
+
44
+ return path;
45
+ } // Gets a value in the object at the path
46
+
47
+ export function get(object, path, find) {
48
+ return parsePropertyPath(path).reduce((target, key) => target === null || target === void 0 ? void 0 : target[key], object);
49
+ } // Sets a value to the object at the path creating any necessary nested
50
+ // objects or arrays along the way
51
+
52
+ export function set(object, path, value) {
53
+ return parsePropertyPath(path).reduce((target, key, index, path) => {
54
+ if (index < path.length - 1) {
55
+ target[key] ?? (target[key] = create(isArrayKey(path[index + 1])));
56
+ return target[key];
57
+ } else {
58
+ target[key] = value;
59
+ return object;
60
+ }
61
+ }, object);
62
+ } // Deletes properties from an object at the paths
63
+
64
+ export function del(object, ...paths) {
65
+ return paths.reduce((object, path) => {
66
+ return parsePropertyPath(path).reduce((target, key, index, path) => {
67
+ if (index < path.length - 1) {
68
+ return target === null || target === void 0 ? void 0 : target[key];
69
+ } else {
70
+ target === null || target === void 0 ? true : delete target[key];
71
+ return object;
72
+ }
73
+ }, object);
74
+ }, object);
75
+ } // Maps a value from one path to another, deleting the first path
76
+
77
+ export function map(object, from, to, transform = v => v) {
78
+ return set(object, to, transform(parsePropertyPath(from).reduce((target, key, index, path) => {
79
+ let value = target === null || target === void 0 ? void 0 : target[key];
80
+
81
+ if (index === path.length - 1) {
82
+ target === null || target === void 0 ? true : delete target[key];
83
+ }
84
+
85
+ return value;
86
+ }, object)));
87
+ } // Steps through an object's properties calling the function with the path and value of each
88
+
89
+ function walk(object, fn, path = []) {
90
+ if (path.length && fn([...path], object) === false) return;
91
+
92
+ if (object != null && typeof object === 'object') {
93
+ let isArrayObject = isArray(object);
94
+
95
+ for (let [key, value] of entries(object)) {
96
+ if (isArrayObject) key = parseInt(key, 10);
97
+ walk(value, fn, [...path, key]);
98
+ }
99
+ }
100
+ } // Recursively mutate and filter empty values from arrays and objects
101
+
102
+
103
+ export function filterEmpty(subject) {
104
+ if (typeof subject === 'object') {
105
+ if (isArray(subject)) {
106
+ for (let i = 0; i < subject.length; i++) {
107
+ if (!filterEmpty(subject[i])) {
108
+ subject.splice(i--, 1);
109
+ }
110
+ }
111
+
112
+ return subject.length > 0;
113
+ } else {
114
+ for (let k in subject) {
115
+ if (!filterEmpty(subject[k])) {
116
+ delete subject[k];
117
+ }
118
+ }
119
+
120
+ return entries(subject).length > 0;
121
+ }
122
+ } else {
123
+ return subject != null;
124
+ }
125
+ } // Merges source values and returns a new merged value. The map function will be called with a
126
+ // property's path, previous value, and next value; it should return an array containing any
127
+ // replacement path and value; when a replacement value not defined, values will be merged.
128
+
129
+ export function merge(sources, map) {
130
+ return sources.reduce((target, source, i) => {
131
+ let isSourceArray = isArray(source);
132
+ walk(source, (path, value) => {
133
+ var _ctx;
134
+
135
+ let ctx = get(target, path.slice(0, -1));
136
+ let key = path[path.length - 1];
137
+ let prev = (_ctx = ctx) === null || _ctx === void 0 ? void 0 : _ctx[key]; // maybe map the property path and/or value
138
+
139
+ let [mapped, next] = (map === null || map === void 0 ? void 0 : map(path, prev, value)) || []; // update the context and path if changed
140
+
141
+ if (mapped !== null && mapped !== void 0 && mapped.some((m, i) => m !== path[i])) {
142
+ ctx = get(target, mapped.slice(0, -1));
143
+ path = [...mapped];
144
+ } // adjust path to concat array values when necessary
145
+
146
+
147
+ if (next !== null && (isArray(ctx) || isInteger(key))) {
148
+ var _ctx2;
149
+
150
+ path.splice(-1, 1, ((_ctx2 = ctx) === null || _ctx2 === void 0 ? void 0 : _ctx2.length) ?? 0);
151
+ } // delete prev values
152
+
153
+
154
+ if (next === null || next == null && value === null) {
155
+ del(target, path);
156
+ } // set the next or default value if there is one
157
+
158
+
159
+ if (next != null || next !== null && value != null && !hasKeys(value)) {
160
+ set(target ?? (target = create(isSourceArray)), path, next ?? value);
161
+ } // do not recurse mapped objects
162
+
163
+
164
+ return next === undefined;
165
+ });
166
+ return target;
167
+ }, undefined);
168
+ }
169
+ export default merge;
@@ -0,0 +1,42 @@
1
+ import merge from './merge.js';
2
+ import { getSchema } from '../validate.js'; // Edge case camelizations
3
+
4
+ const CAMELCASE_MAP = new Map([['css', 'CSS'], ['javascript', 'JavaScript']]); // Converts kebab-cased and snake_cased strings to camelCase.
5
+
6
+ const KEBAB_SNAKE_REG = /[-_]([^-_]+)/g;
7
+ export function camelcase(str) {
8
+ if (typeof str !== 'string') return str;
9
+ return str.replace(KEBAB_SNAKE_REG, (match, word) => CAMELCASE_MAP.get(word) || word[0].toUpperCase() + word.slice(1));
10
+ } // Coverts camelCased and snake_cased strings to kebab-case.
11
+
12
+ const CAMEL_SNAKE_REG = /([a-z])([A-Z]+)|_([^_]+)/g;
13
+ export function kebabcase(str) {
14
+ if (typeof str !== 'string') return str;
15
+ return Array.from(CAMELCASE_MAP).reduce((str, [word, camel]) => str.replace(camel, `-${word}`), str).replace(CAMEL_SNAKE_REG, (match, p, n, w) => `${p || ''}-${(n || w).toLowerCase()}`);
16
+ } // Removes undefined empty values and renames kebab-case properties to camelCase. Optionally
17
+ // allows deep merging with options.overrides, converting keys to kebab-case with options.kebab,
18
+ // and normalizing against a schema with options.schema.
19
+
20
+ export function normalize(object, options) {
21
+ var _options, _options2;
22
+
23
+ if (typeof options === 'string') options = {
24
+ schema: options
25
+ };
26
+ let keycase = (_options = options) !== null && _options !== void 0 && _options.kebab ? kebabcase : camelcase;
27
+ return merge([object, (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.overrides], path => {
28
+ var _options3, _schemas$shift;
29
+
30
+ let schemas = getSchema((_options3 = options) === null || _options3 === void 0 ? void 0 : _options3.schema, path.map(camelcase));
31
+ let skip = ((_schemas$shift = schemas.shift()) === null || _schemas$shift === void 0 ? void 0 : _schemas$shift.normalize) === false;
32
+ path = path.map((k, i) => {
33
+ var _schemas$i;
34
+
35
+ if (skip) return k;
36
+ skip || (skip = ((_schemas$i = schemas[i]) === null || _schemas$i === void 0 ? void 0 : _schemas$i.normalize) === false);
37
+ return keycase(k);
38
+ });
39
+ return [path];
40
+ });
41
+ }
42
+ export default normalize;
@@ -0,0 +1,29 @@
1
+ import util from 'util';
2
+ import YAML from 'yaml';
3
+ import getDefaults from '../defaults.js'; // Provides native util.inspect with common options for printing configs.
4
+
5
+ export function inspect(config) {
6
+ return util.inspect(config, {
7
+ depth: null,
8
+ compact: false
9
+ });
10
+ } // Converts a config to a yaml, json, or js string. When no config is provided,
11
+ // falls back to schema defaults.
12
+
13
+ export function stringify(format, config = getDefaults()) {
14
+ switch (format) {
15
+ case 'yml':
16
+ case 'yaml':
17
+ return YAML.stringify(config);
18
+
19
+ case 'json':
20
+ return JSON.stringify(config, null, 2) + '\n';
21
+
22
+ case 'js':
23
+ return `module.exports = ${inspect(config)}\n`;
24
+
25
+ default:
26
+ throw new Error(`Unsupported format: ${format}`);
27
+ }
28
+ }
29
+ export default stringify;
@@ -0,0 +1,238 @@
1
+ import AJV from 'ajv/dist/2019.js';
2
+ import { set, del, filterEmpty, parsePropertyPath, joinPropertyPath, isArrayKey } from './utils/index.js';
3
+ const {
4
+ isArray
5
+ } = Array;
6
+ const {
7
+ assign,
8
+ entries
9
+ } = Object; // AJV manages and validates schemas.
10
+
11
+ const ajv = new AJV({
12
+ strict: false,
13
+ verbose: true,
14
+ allErrors: true,
15
+ schemas: [getDefaultSchema()],
16
+ keywords: [{
17
+ // custom instanceof schema validation
18
+ keyword: 'instanceof',
19
+ metaSchema: {
20
+ enum: ['Function', 'RegExp']
21
+ },
22
+ error: {
23
+ message: cxt => AJV.str`must be an instanceof ${cxt.schemaCode}`,
24
+ params: cxt => AJV._`{ instanceof: ${cxt.schemaCode} }`
25
+ },
26
+ code: cxt => cxt.fail(AJV._`!(${cxt.data} instanceof ${AJV._([cxt.schema])})`)
27
+ }, {
28
+ // disallowed validation based on required
29
+ keyword: 'disallowed',
30
+ metaSchema: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'string'
34
+ }
35
+ },
36
+ error: {
37
+ message: 'disallowed property',
38
+ params: cxt => AJV._`{ disallowedProperty: ${cxt.params.disallowedProperty} }`
39
+ },
40
+ code: cxt => {
41
+ let {
42
+ data,
43
+ gen,
44
+ schema
45
+ } = cxt;
46
+
47
+ for (let prop of schema) {
48
+ gen.if(AJV._`${data}.${AJV._([prop])} !== undefined`, () => {
49
+ cxt.setParams({
50
+ disallowedProperty: AJV._`${prop}`
51
+ }, true);
52
+ cxt.error();
53
+ });
54
+ }
55
+ }
56
+ }]
57
+ }); // Returns a new default schema.
58
+
59
+ function getDefaultSchema() {
60
+ return {
61
+ $id: '/config',
62
+ type: 'object',
63
+ additionalProperties: false,
64
+ properties: {
65
+ version: {
66
+ type: 'integer',
67
+ default: 2
68
+ }
69
+ }
70
+ };
71
+ } // Gets the schema object from the AJV schema. If a path is provided, an array of schemas is
72
+ // returned, with each index representing the schema of each part of the path (index zero is root).
73
+
74
+
75
+ export function getSchema(name, path, root) {
76
+ var _ajv$getSchema, _ref, _schema$properties;
77
+
78
+ // get the root schema if necessary, resolve it, and return it when there is no path
79
+ let schema = typeof name === 'string' ? (_ajv$getSchema = ajv.getSchema(name)) === null || _ajv$getSchema === void 0 ? void 0 : _ajv$getSchema.schema : name;
80
+ if (!schema || !path) return schema ?? [];
81
+ root ?? (root = schema); // parse and work with one key at a time
82
+
83
+ let [key, ...rest] = path = parsePropertyPath(path); // if the desired schema is one of many, we need to find the best match
84
+
85
+ let many = (_ref = isArray(schema) ? schema : schema === null || schema === void 0 ? void 0 : schema[['anyOf', 'oneOf', 'allOf'].find(p => schema[p])]) === null || _ref === void 0 ? void 0 : _ref.map(p => getSchema(p, path, root)).sort((a, b) => {
86
+ var _a$;
87
+
88
+ return (// the best possible match will match most of the path or loosely match
89
+ b.length - a.length || (((_a$ = a[0]) === null || _a$ === void 0 ? void 0 : _a$.type) === 'object' && (a[0].additionalProperties !== false || a[0].unevaluatedProperties !== false) ? -1 : 1)
90
+ );
91
+ })[0];
92
+
93
+ if (many !== null && many !== void 0 && many.length) {
94
+ return many;
95
+ } else if ((schema === null || schema === void 0 ? void 0 : schema.type) === 'array' && isArrayKey(key)) {
96
+ // find the remaining paths in the items schema
97
+ return [schema].concat(getSchema(schema.items, rest, root));
98
+ } else if ((schema === null || schema === void 0 ? void 0 : schema.type) === 'object' && path.length && (_schema$properties = schema.properties) !== null && _schema$properties !== void 0 && _schema$properties[key]) {
99
+ // find the remaining paths nested in the property schema
100
+ return [schema].concat(getSchema(schema.properties[key], rest, root));
101
+ } else if (schema !== null && schema !== void 0 && schema.$ref && (path.length || Object.keys(schema).length === 1)) {
102
+ // follow references
103
+ let $ref = schema.$ref.startsWith('#') ? `${root.$id}${schema.$ref}` : schema.$ref;
104
+ return getSchema($ref, path, root);
105
+ } else if (schema && (!path.length || schema.type === 'object' && schema.additionalProperties !== false)) {
106
+ // end of path matching
107
+ return [schema];
108
+ } else {
109
+ // no match
110
+ return [];
111
+ }
112
+ } // Adds schemas to the config schema's properties. The config schema is removed, modified, and
113
+ // replaced after the new schemas are added to clear any compiled caches. Existing schemas are
114
+ // removed and replaced as well. If a schema has an existing $id, the schema will not be added
115
+ // as config schema properties.
116
+
117
+ export function addSchema(schemas) {
118
+ if (isArray(schemas)) {
119
+ return schemas.map(addSchema);
120
+ }
121
+
122
+ if (schemas.$id) {
123
+ let {
124
+ $id
125
+ } = schemas;
126
+ if (ajv.getSchema($id)) ajv.removeSchema($id);
127
+ return ajv.addSchema(schemas);
128
+ }
129
+
130
+ let config = getSchema('/config');
131
+ ajv.removeSchema('/config');
132
+
133
+ for (let [key, schema] of entries(schemas)) {
134
+ let $id = `/config/${key}`;
135
+ if (ajv.getSchema($id)) ajv.removeSchema($id);
136
+ assign(config.properties, {
137
+ [key]: {
138
+ $ref: $id
139
+ }
140
+ });
141
+ ajv.addSchema(schema, $id);
142
+ }
143
+
144
+ return ajv.addSchema(config, '/config');
145
+ } // Resets the schema by removing all schemas and inserting a new default schema.
146
+
147
+ export function resetSchema() {
148
+ ajv.removeSchema();
149
+ ajv.addSchema(getDefaultSchema(), '/config');
150
+ } // Adds "a" or "an" to a word for readability.
151
+
152
+ function a(word) {
153
+ if (word === 'undefined' || word === 'null') return word;
154
+ return `${'aeiou'.includes(word[0]) ? 'an' : 'a'} ${word}`;
155
+ } // Default errors anywhere within these keywords can be confusing
156
+
157
+
158
+ const HIDE_NESTED_KEYWORDS = ['oneOf', 'anyOf', 'allOf', 'not'];
159
+
160
+ function shouldHideError(key, path, error) {
161
+ var _parentSchema$errors;
162
+
163
+ let {
164
+ parentSchema,
165
+ keyword,
166
+ schemaPath
167
+ } = error;
168
+ return !(parentSchema.error || (_parentSchema$errors = parentSchema.errors) !== null && _parentSchema$errors !== void 0 && _parentSchema$errors[keyword]) && (HIDE_NESTED_KEYWORDS.some(k => schemaPath.includes(`/${k}`)) || getSchema(key, path)[path.length] !== parentSchema);
169
+ } // Validates data according to the associated schema and returns a list of errors, if any.
170
+
171
+
172
+ export function validate(data, key = '/config') {
173
+ if (!ajv.validate(key, data)) {
174
+ let errors = new Map();
175
+
176
+ for (let error of ajv.errors) {
177
+ var _parentSchema$errors2;
178
+
179
+ let {
180
+ instancePath,
181
+ parentSchema,
182
+ keyword,
183
+ message,
184
+ params
185
+ } = error;
186
+ let path = instancePath ? instancePath.substr(1).split('/') : [];
187
+ if (shouldHideError(key, path, error)) continue; // generate a custom error message
188
+
189
+ if (parentSchema.error || (_parentSchema$errors2 = parentSchema.errors) !== null && _parentSchema$errors2 !== void 0 && _parentSchema$errors2[keyword]) {
190
+ let custom = parentSchema.error || parentSchema.errors[keyword];
191
+ message = typeof custom === 'function' ? custom(error) : custom;
192
+ } else if (keyword === 'type') {
193
+ let dataType = error.data === null ? 'null' : isArray(error.data) ? 'array' : typeof error.data;
194
+ message = `must be ${a(params.type)}, received ${a(dataType)}`;
195
+ } else if (keyword === 'required') {
196
+ message = 'missing required property';
197
+ } else if (keyword === 'additionalProperties' || keyword === 'unevaluatedProperties') {
198
+ message = 'unknown property';
199
+ } // fix paths
200
+
201
+
202
+ if (params.missingProperty) {
203
+ path.push(params.missingProperty);
204
+ } else if (params.additionalProperty) {
205
+ path.push(params.additionalProperty);
206
+ } else if (params.unevaluatedProperty) {
207
+ path.push(params.unevaluatedProperty);
208
+ } else if (params.disallowedProperty) {
209
+ path.push(params.disallowedProperty);
210
+ } // fix invalid data
211
+
212
+
213
+ if (keyword === 'minimum') {
214
+ set(data, path, Math.max(error.data, error.schema));
215
+ } else if (keyword === 'maximum') {
216
+ set(data, path, Math.min(error.data, error.schema));
217
+ } else if (keyword === 'required') {
218
+ del(data, path.slice(0, -1));
219
+ } else {
220
+ del(data, path);
221
+ } // joined for error messages
222
+
223
+
224
+ path = joinPropertyPath(path); // map one error per path
225
+
226
+ errors.set(path, {
227
+ path,
228
+ message
229
+ });
230
+ } // filter empty values as a result of scrubbing
231
+
232
+
233
+ filterEmpty(data); // return an array of errors
234
+
235
+ return Array.from(errors.values());
236
+ }
237
+ }
238
+ export default validate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/config",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,8 +14,8 @@
14
14
  "node": ">=14"
15
15
  },
16
16
  "files": [
17
- "./dist",
18
- "./types/index.d.ts"
17
+ "dist",
18
+ "types/index.d.ts"
19
19
  ],
20
20
  "main": "./dist/index.js",
21
21
  "types": "./types/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "test:types": "tsd"
34
34
  },
35
35
  "dependencies": {
36
- "@percy/logger": "1.0.0",
36
+ "@percy/logger": "1.0.1",
37
37
  "ajv": "^8.6.2",
38
38
  "cosmiconfig": "^7.0.0",
39
39
  "yaml": "^1.10.0"
@@ -41,5 +41,5 @@
41
41
  "devDependencies": {
42
42
  "json-schema-typed": "^7.0.3"
43
43
  },
44
- "gitHead": "6df509421a60144e4f9f5d59dc57a5675372a0b2"
44
+ "gitHead": "38917e6027299d6cd86008e2ccd005d90bbf89c0"
45
45
  }
@@ -0,0 +1,33 @@
1
+ import { JSONSchema } from 'json-schema-typed';
2
+
3
+ interface Pojo { [x: string]: any; }
4
+ export interface PercyConfigObject extends Pojo { version: number; }
5
+
6
+ declare namespace PercyConfig {
7
+ function load(options?: {
8
+ path?: undefined | string | false,
9
+ overrides?: Pojo,
10
+ reload?: boolean,
11
+ bail?: boolean
12
+ }): PercyConfigObject;
13
+
14
+ function validate(
15
+ config: PercyConfigObject,
16
+ options?: { scrub?: boolean }
17
+ ): boolean;
18
+
19
+ function addSchema(schemas: {
20
+ [x: string]: JSONSchema
21
+ }): void;
22
+
23
+ function getDefaults(
24
+ overrides?: Pojo
25
+ ): PercyConfigObject;
26
+
27
+ function stringify(
28
+ format: 'yaml' | 'json' | 'js',
29
+ config?: PercyConfigObject
30
+ ): string;
31
+ }
32
+
33
+ export default PercyConfig