@percy/config 1.0.0-beta.7 → 1.0.0-beta.73

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.
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
- ## @percy/config
1
+ # @percy/config
2
2
 
3
3
  Handles loading and adding options to Percy configuration files. Uses
4
4
  [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) to load configuration files and [JSON
5
5
  schema](https://json-schema.org/) with [AJV](https://github.com/epoberezkin/ajv) to validate those
6
6
  configuration files.
7
7
 
8
- ## Usage
8
+ - [Loading config files](#loading-config-files)
9
+ - [Extending config options](#extending-config-options)
9
10
 
10
- ### Loading config files
11
+ ## Loading config files
11
12
 
12
13
  The `.load()` method will load and validate a configuation file, optionally merging it with any
13
14
  provided `overrides`. If no `path` is provided, will search for the first supported config found
@@ -18,14 +19,17 @@ from the current directory up to the home directoy. Configuration files are cach
18
19
  import PercyConfig from '@percy/config'
19
20
 
20
21
  // loading is done synchronously
21
- const config = PercyConfig.load({
22
- path, // config file path or directory path containing a config file
23
- overrides = {}, // configuration option overrides
24
- reload = false, // reload file and update cache
25
- bail = false // return undefined on validation warnings
26
- })
22
+ const config = PercyConfig.load(options)
27
23
  ```
28
24
 
25
+ #### Options
26
+
27
+ - `path` — Config file path or directory containing a config file
28
+ - `overrides` — Config option overrides
29
+ - `reload` — Do not use cached config (**default** `false`)
30
+ - `bail` — Return undefined when failing validation (**default** `false`)
31
+ - `print` — Print info and error logs (**default** `false`)
32
+
29
33
  #### Supported files
30
34
 
31
35
  - `"percy"` entry in `package.json`
@@ -34,7 +38,7 @@ const config = PercyConfig.load({
34
38
  - `.percy.yaml` or `.percy.yml` YAML file
35
39
  - `.percy.js` or `percy.config.js` file that exports an object
36
40
 
37
- ### Extending config options
41
+ ## Extending config options
38
42
 
39
43
  The `.addSchema()` function will add a sub-schema to the Percy configuration file which will be
40
44
  parsed and validated when `PercyConfig.load()` is called. See [JSON
@@ -43,5 +47,7 @@ schema](https://json-schema.org/) for possible schema options.
43
47
  ```js
44
48
  import PercyConfig from '@percy/config'
45
49
 
46
- PercyConfig.addSchema({ propertyName: JSONSchema })
50
+ PercyConfig.addSchema({
51
+ propertyName: JSONSchema
52
+ })
47
53
  ```
package/dist/defaults.js CHANGED
@@ -3,46 +3,50 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = getDefaults;
6
+ exports.default = void 0;
7
+ exports.getDefaults = getDefaults;
7
8
 
8
- var _deepmerge = _interopRequireDefault(require("deepmerge"));
9
+ var _utils = require("./utils");
9
10
 
10
11
  var _validate = require("./validate");
11
12
 
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
-
13
+ const {
14
+ isArray
15
+ } = Array;
14
16
  const {
15
17
  assign,
16
- entries,
17
- freeze
18
+ entries
18
19
  } = Object; // Recursively walks a schema and collects defaults. When no schema is provided,
19
- // the default config schema is used. Returned defaults are frozen.
20
+ // the default config schema is used.
20
21
 
21
- function getDefaultFromSchema(schema) {
22
+ function getDefaultsFromSchema(schema) {
22
23
  if (!schema || typeof schema.$ref === 'string') {
23
24
  var _schema$$ref;
24
25
 
25
26
  // get the schema from ajv
26
- return getDefaultFromSchema((0, _validate.getSchema)((_schema$$ref = schema === null || schema === void 0 ? void 0 : schema.$ref) !== null && _schema$$ref !== void 0 ? _schema$$ref : 'config'));
27
+ return getDefaultsFromSchema((0, _validate.getSchema)((_schema$$ref = schema === null || schema === void 0 ? void 0 : schema.$ref) !== null && _schema$$ref !== void 0 ? _schema$$ref : '/config'));
27
28
  } else if (schema.default != null) {
28
- // return the frozen default for this schema
29
- return freeze(schema.default);
29
+ // return the default for this schema
30
+ return schema.default;
30
31
  } else if (schema.type === 'object' && schema.properties) {
31
- // return a frozen object of default properties
32
- return freeze(entries(schema.properties).reduce((acc, [prop, schema]) => {
33
- let def = getDefaultFromSchema(schema);
32
+ // return an object of default properties
33
+ return entries(schema.properties).reduce((acc, [prop, schema]) => {
34
+ let def = getDefaultsFromSchema(schema);
34
35
  return def != null ? assign(acc || {}, {
35
36
  [prop]: def
36
37
  }) : acc;
37
- }, undefined));
38
+ }, undefined);
38
39
  } else {
39
40
  return undefined;
40
41
  }
41
42
  }
42
43
 
43
44
  function getDefaults(overrides = {}) {
44
- return _deepmerge.default.all([getDefaultFromSchema(), overrides], {
45
- // overwrite default arrays, do not merge
46
- arrayMerge: (_, arr) => arr
45
+ return (0, _utils.merge)([getDefaultsFromSchema(), overrides], (path, prev, next) => {
46
+ // override default array instead of merging
47
+ return isArray(next) && [path, next];
47
48
  });
48
- }
49
+ }
50
+
51
+ var _default = getDefaults;
52
+ exports.default = _default;
package/dist/index.js CHANGED
@@ -3,31 +3,118 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "addMigration", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _migrate.addMigration;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "addSchema", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _validate.addSchema;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "cache", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _load.cache;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "clearMigrations", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _migrate.clearMigrations;
28
+ }
29
+ });
6
30
  exports.default = void 0;
31
+ Object.defineProperty(exports, "explorer", {
32
+ enumerable: true,
33
+ get: function () {
34
+ return _load.explorer;
35
+ }
36
+ });
37
+ Object.defineProperty(exports, "getDefaults", {
38
+ enumerable: true,
39
+ get: function () {
40
+ return _defaults.default;
41
+ }
42
+ });
43
+ Object.defineProperty(exports, "load", {
44
+ enumerable: true,
45
+ get: function () {
46
+ return _load.default;
47
+ }
48
+ });
49
+ Object.defineProperty(exports, "migrate", {
50
+ enumerable: true,
51
+ get: function () {
52
+ return _migrate.default;
53
+ }
54
+ });
55
+ Object.defineProperty(exports, "normalize", {
56
+ enumerable: true,
57
+ get: function () {
58
+ return _normalize.default;
59
+ }
60
+ });
61
+ Object.defineProperty(exports, "resetSchema", {
62
+ enumerable: true,
63
+ get: function () {
64
+ return _validate.resetSchema;
65
+ }
66
+ });
67
+ Object.defineProperty(exports, "search", {
68
+ enumerable: true,
69
+ get: function () {
70
+ return _load.search;
71
+ }
72
+ });
73
+ Object.defineProperty(exports, "stringify", {
74
+ enumerable: true,
75
+ get: function () {
76
+ return _stringify.default;
77
+ }
78
+ });
79
+ Object.defineProperty(exports, "validate", {
80
+ enumerable: true,
81
+ get: function () {
82
+ return _validate.default;
83
+ }
84
+ });
7
85
 
8
86
  var _load = _interopRequireWildcard(require("./load"));
9
87
 
10
88
  var _validate = _interopRequireWildcard(require("./validate"));
11
89
 
90
+ var _migrate = _interopRequireWildcard(require("./migrate"));
91
+
12
92
  var _defaults = _interopRequireDefault(require("./defaults"));
13
93
 
94
+ var _normalize = _interopRequireDefault(require("./normalize"));
95
+
14
96
  var _stringify = _interopRequireDefault(require("./stringify"));
15
97
 
16
98
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
99
 
18
- function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
100
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
19
101
 
20
- function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
102
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
21
103
 
22
- // Export a single object that can be imported as PercyConfig
104
+ // mirror the namespace as the default export
23
105
  var _default = {
24
106
  load: _load.default,
107
+ search: _load.search,
25
108
  cache: _load.cache,
26
109
  explorer: _load.explorer,
27
110
  validate: _validate.default,
28
111
  addSchema: _validate.addSchema,
29
112
  resetSchema: _validate.resetSchema,
113
+ migrate: _migrate.default,
114
+ addMigration: _migrate.addMigration,
115
+ clearMigrations: _migrate.clearMigrations,
30
116
  getDefaults: _defaults.default,
117
+ normalize: _normalize.default,
31
118
  stringify: _stringify.default
32
119
  };
33
120
  exports.default = _default;
package/dist/load.js CHANGED
@@ -3,27 +3,28 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = load;
7
- exports.explorer = exports.cache = void 0;
6
+ exports.explorer = exports.default = exports.cache = void 0;
7
+ exports.load = load;
8
+ exports.search = search;
8
9
 
9
10
  var _path = require("path");
10
11
 
11
- var _cosmiconfig = require("cosmiconfig");
12
-
13
- var _pathType = require("path-type");
12
+ var _fs = require("fs");
14
13
 
15
- var _deepmerge = _interopRequireDefault(require("deepmerge"));
14
+ var _cosmiconfig = require("cosmiconfig");
16
15
 
17
16
  var _logger = _interopRequireDefault(require("@percy/logger"));
18
17
 
19
18
  var _defaults = _interopRequireDefault(require("./defaults"));
20
19
 
21
- var _normalize = _interopRequireDefault(require("./normalize"));
20
+ var _migrate = _interopRequireDefault(require("./migrate"));
22
21
 
23
- var _validate = _interopRequireDefault(require("./validate"));
22
+ var _normalize = _interopRequireDefault(require("./normalize"));
24
23
 
25
24
  var _stringify = require("./stringify");
26
25
 
26
+ var _validate = _interopRequireDefault(require("./validate"));
27
+
27
28
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
29
 
29
30
  // Loaded configuration file cache
@@ -33,60 +34,94 @@ exports.cache = cache;
33
34
  const explorer = (0, _cosmiconfig.cosmiconfigSync)('percy', {
34
35
  cache: false,
35
36
  searchPlaces: ['package.json', '.percyrc', '.percy.json', '.percy.yaml', '.percy.yml', '.percy.js', 'percy.config.js']
36
- }); // Finds and loads a config file using cosmiconfig, merges it with optional
37
+ }); // Searches within a provided directory, or loads the provided config path
38
+
39
+ exports.explorer = explorer;
40
+
41
+ function search(path) {
42
+ try {
43
+ let result = path && !(0, _fs.statSync)(path).isDirectory() ? explorer.load(path) : explorer.search(path);
44
+ return result || {};
45
+ } catch (error) {
46
+ if (error.code === 'ENOENT') return {};else throw error;
47
+ }
48
+ } // Finds and loads a config file using cosmiconfig, merges it with optional
37
49
  // inputs, validates the combined config according to the schema, and returns
38
50
  // the combined config. Loaded config files are cached and reused on next load,
39
51
  // unless `reload` is true in which the file will be reloaded and the cache
40
52
  // updated. Validation errors are logged as warnings and the config is returned
41
53
  // unless `bail` is true. Supports kebab-case and camelCase config options and
42
- // always returns camelCase options. Currently only supports version 2 config
43
- // files; missing versions or other versions are discarded.
54
+ // always returns camelCase options. Will automatically convert older config
55
+ // versions to the latest version while printing a warning.
44
56
 
45
- exports.explorer = explorer;
46
57
 
47
58
  function load({
48
59
  path,
49
60
  overrides = {},
50
61
  reload = false,
51
- bail = false
62
+ bail = false,
63
+ print = false
52
64
  } = {}) {
53
65
  var _Array$from;
54
66
 
55
67
  // load cached config; when no path is specified, get the last config cached
56
- let config = path ? cache.get(path) : (_Array$from = Array.from(cache)[cache.size - 1]) === null || _Array$from === void 0 ? void 0 : _Array$from[1]; // load config or reload cached config
68
+ let config = path ? cache.get(path) : (_Array$from = Array.from(cache)[cache.size - 1]) === null || _Array$from === void 0 ? void 0 : _Array$from[1];
69
+ let infoDebug = print ? 'info' : 'debug';
70
+ let errorDebug = print ? 'error' : 'debug';
71
+ let log = (0, _logger.default)('config'); // load config or reload cached config
57
72
 
58
73
  if (path !== false && (!config || reload)) {
59
74
  try {
60
- let result = !path || (0, _pathType.isDirectorySync)(path) ? explorer.search(path) : explorer.load(path);
75
+ let result = search(path);
61
76
 
62
- if (result && result.config) {
63
- _logger.default.debug(`Found config file: ${(0, _path.relative)('', result.filepath)}`);
77
+ if (result !== null && result !== void 0 && result.config) {
78
+ log[infoDebug](`Found config file: ${(0, _path.relative)('', result.filepath)}`);
79
+ let version = parseInt(result.config.version, 10);
64
80
 
65
- if (result.config.version !== 2) {
66
- _logger.default.warn('Ignoring config file - ' + (!result.config.version ? 'missing version' : 'unsupported version'));
81
+ if (Number.isNaN(version)) {
82
+ log.warn('Ignoring config file - missing or invalid version');
83
+ } else if (version > 2) {
84
+ log.warn(`Ignoring config file - unsupported version "${version}"`);
67
85
  } else {
68
- // normalize to remove empty values and convert snake-case to camelCase
69
- config = (0, _normalize.default)(result.config);
86
+ if (version < 2) {
87
+ log.warn('Found older config file version, please run ' + '`percy config:migrate` to update to the latest version');
88
+ }
89
+
90
+ config = (0, _migrate.default)(result.config);
70
91
  cache.set(path, config);
71
92
  }
72
93
  } else {
73
- _logger.default.debug('Config file not found');
94
+ log[infoDebug]('Config file not found');
95
+ if (bail) return;
74
96
  }
75
97
  } catch (error) {
76
- _logger.default.debug('Failed to load or parse config file');
77
-
78
- _logger.default.debug(error.toString());
98
+ log[errorDebug](error);
99
+ if (bail) return;
79
100
  }
80
- } // merge found config with overrides and validate
101
+ } // normalize and merge with overrides then validate
102
+
81
103
 
104
+ config = (0, _normalize.default)(config, {
105
+ overrides,
106
+ schema: '/config'
107
+ });
108
+ let errors = config && (0, _validate.default)(config);
82
109
 
83
- config = (0, _deepmerge.default)(config || {}, overrides);
84
- if (!(0, _validate.default)(config, {
85
- scrub: true
86
- }) && bail) return; // normalize again to remove empty values from overrides and validation scrubbing
110
+ if (errors) {
111
+ log.warn('Invalid config:');
112
+
113
+ for (let e of errors) log.warn(`- ${e.path}: ${e.message}`);
114
+
115
+ if (bail) return;
116
+ }
117
+
118
+ if (path !== false && config) {
119
+ log[infoDebug](`Using config:\n${(0, _stringify.inspect)(config)}`);
120
+ } // merge with defaults
87
121
 
88
- config = (0, _normalize.default)(config);
89
- if (config) _logger.default.debug(`Using config:\n${(0, _stringify.inspect)(config)}`); // merge with defaults
90
122
 
91
123
  return (0, _defaults.default)(config);
92
- }
124
+ }
125
+
126
+ var _default = load;
127
+ exports.default = _default;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.addMigration = addMigration;
7
+ exports.clearMigrations = clearMigrations;
8
+ exports.default = void 0;
9
+ exports.migrate = migrate;
10
+
11
+ var _logger = _interopRequireDefault(require("@percy/logger"));
12
+
13
+ var _normalize2 = _interopRequireDefault(require("./normalize"));
14
+
15
+ var _utils = require("./utils");
16
+
17
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
+
19
+ // Global set of registered migrations
20
+ const migrations = new Map(); // Register a migration function for the main config schema by default
21
+
22
+ function addMigration(migration, schema = '/config') {
23
+ if (Array.isArray(migration)) {
24
+ // accept schema as the first item in a tuple
25
+ if (typeof migration[0] === 'string') [schema, ...migration] = migration;
26
+ return migration.map(m => addMigration(m, schema));
27
+ }
28
+
29
+ if (!migrations.has(schema)) migrations.set(schema, []);
30
+ migrations.get(schema).unshift(migration);
31
+ return migration;
32
+ } // Clear all migration functions
33
+
34
+
35
+ function clearMigrations() {
36
+ migrations.clear();
37
+ addMigration(defaultMigration);
38
+ } // The default config migration
39
+
40
+
41
+ addMigration(defaultMigration);
42
+
43
+ function defaultMigration(config, {
44
+ set
45
+ }) {
46
+ if (config.version !== 2) set('version', 2);
47
+ } // Migrate util for deprecated options
48
+
49
+
50
+ function deprecate(config, log, path, options) {
51
+ if ((0, _utils.get)(config, path) == null) return;
52
+ let {
53
+ type,
54
+ until: ver,
55
+ map: to,
56
+ alt,
57
+ warn
58
+ } = options;
59
+ let name = (0, _utils.joinPropertyPath)(path);
60
+ let message = 'The ' + [type ? `${type} option \`${name}\`` : `\`${name}\` option`, `will be removed in ${ver || 'a future release'}.`, to ? `Use \`${to}\` instead.` : alt || ''].join(' ').trim();
61
+ if (warn) log.warn(`Warning: ${message}`);else log.deprecated(message);
62
+ return to ? (0, _utils.map)(config, path, to) : config;
63
+ } // Calls each registered migration function with a normalize provided config
64
+ // and util functions for working with the config object
65
+
66
+
67
+ function migrate(config, schema = '/config') {
68
+ var _normalize;
69
+
70
+ config = (_normalize = (0, _normalize2.default)(config, {
71
+ schema
72
+ })) !== null && _normalize !== void 0 ? _normalize : {};
73
+
74
+ if (migrations.has(schema)) {
75
+ let log = (0, _logger.default)('config');
76
+ let util = {
77
+ deprecate: deprecate.bind(null, config, log),
78
+ set: _utils.set.bind(null, config),
79
+ map: _utils.map.bind(null, config),
80
+ del: _utils.del.bind(null, config),
81
+ log
82
+ };
83
+
84
+ for (let migration of migrations.get(schema)) {
85
+ migration(config, util);
86
+ } // normalize again to adjust for migrations
87
+
88
+
89
+ config = (0, _normalize2.default)(config, {
90
+ schema
91
+ });
92
+ }
93
+
94
+ return config;
95
+ }
96
+
97
+ var _default = migrate;
98
+ exports.default = _default;
package/dist/normalize.js CHANGED
@@ -3,35 +3,53 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = normalize;
7
-
8
- // recursively reduces config objects and arrays to remove undefined and empty
9
- // values and rename kebab-case properties to camelCase.
10
- function normalize(subject) {
11
- if (typeof subject === 'object') {
12
- let isArray = Array.isArray(subject);
13
- return Object.entries(subject).reduce((result, [key, value]) => {
14
- value = normalize(value);
15
-
16
- if (typeof value !== 'undefined') {
17
- return isArray ? (result || []).concat(value) : Object.assign(result || {}, {
18
- [camelize(key)]: value
19
- });
20
- } else {
21
- return result;
22
- }
23
- }, undefined);
24
- } else {
25
- return subject;
26
- }
27
- } // Edge case camelizations
28
-
29
-
30
- const CAMELIZE_MAP = {
31
- css: 'CSS',
32
- javascript: 'JavaScript'
33
- }; // Converts a kebab-cased string to camelCase.
34
-
35
- function camelize(s) {
36
- return s.replace(/-([^-]+)/g, (_, w) => CAMELIZE_MAP[w] || w[0].toUpperCase() + w.slice(1));
37
- }
6
+ exports.camelcase = camelcase;
7
+ exports.default = void 0;
8
+ exports.kebabcase = kebabcase;
9
+ exports.normalize = normalize;
10
+
11
+ var _validate = require("./validate");
12
+
13
+ var _utils = require("./utils");
14
+
15
+ // Edge case camelizations
16
+ const CAMELCASE_MAP = new Map([['css', 'CSS'], ['javascript', 'JavaScript']]); // Converts kebab-cased and snake_cased strings to camelCase.
17
+
18
+ const KEBAB_SNAKE_REG = /[-_]([^-_]+)/g;
19
+
20
+ function camelcase(str) {
21
+ if (typeof str !== 'string') return str;
22
+ return str.replace(KEBAB_SNAKE_REG, (match, word) => CAMELCASE_MAP.get(word) || word[0].toUpperCase() + word.slice(1));
23
+ } // Coverts camelCased and snake_cased strings to kebab-case.
24
+
25
+
26
+ const CAMEL_SNAKE_REG = /([a-z])([A-Z]+)|_([^_]+)/g;
27
+
28
+ function kebabcase(str) {
29
+ if (typeof str !== 'string') return str;
30
+ 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()}`);
31
+ } // Removes undefined empty values and renames kebab-case properties to camelCase. Optionally
32
+ // allows deep merging with options.overrides, converting keys to kebab-case with options.kebab,
33
+ // and normalizing against a schema with options.schema.
34
+
35
+
36
+ function normalize(object, options) {
37
+ let keycase = options !== null && options !== void 0 && options.kebab ? kebabcase : camelcase;
38
+ return (0, _utils.merge)([object, options === null || options === void 0 ? void 0 : options.overrides], path => {
39
+ var _schemas$shift;
40
+
41
+ let schemas = (0, _validate.getSchema)(options === null || options === void 0 ? void 0 : options.schema, path.map(camelcase));
42
+ let skip = ((_schemas$shift = schemas.shift()) === null || _schemas$shift === void 0 ? void 0 : _schemas$shift.normalize) === false;
43
+ path = path.map((k, i) => {
44
+ var _schemas$i;
45
+
46
+ if (skip) return k;
47
+ skip || (skip = ((_schemas$i = schemas[i]) === null || _schemas$i === void 0 ? void 0 : _schemas$i.normalize) === false);
48
+ return keycase(k);
49
+ });
50
+ return [path];
51
+ });
52
+ }
53
+
54
+ var _default = normalize;
55
+ exports.default = _default;
package/dist/stringify.js CHANGED
@@ -3,8 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.default = void 0;
6
7
  exports.inspect = inspect;
7
- exports.default = stringify;
8
+ exports.stringify = stringify;
8
9
 
9
10
  var _util = _interopRequireDefault(require("util"));
10
11
 
@@ -31,12 +32,15 @@ function stringify(format, config = (0, _defaults.default)()) {
31
32
  return _yaml.default.stringify(config);
32
33
 
33
34
  case 'json':
34
- return JSON.stringify(config, null, 2);
35
+ return JSON.stringify(config, null, 2) + '\n';
35
36
 
36
37
  case 'js':
37
- return `module.exports = ${inspect(config)}`;
38
+ return `module.exports = ${inspect(config)}\n`;
38
39
 
39
40
  default:
40
41
  throw new Error(`Unsupported format: ${format}`);
41
42
  }
42
- }
43
+ }
44
+
45
+ var _default = stringify;
46
+ exports.default = _default;
package/dist/utils.js ADDED
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.del = del;
7
+ exports.filterEmpty = filterEmpty;
8
+ exports.get = get;
9
+ exports.isArrayKey = isArrayKey;
10
+ exports.joinPropertyPath = joinPropertyPath;
11
+ exports.map = map;
12
+ exports.merge = merge;
13
+ exports.parsePropertyPath = parsePropertyPath;
14
+ exports.set = set;
15
+ const {
16
+ isArray
17
+ } = Array;
18
+ const {
19
+ isInteger
20
+ } = Number;
21
+ const {
22
+ entries
23
+ } = Object; // Creates an empty object or array
24
+
25
+ function create(array) {
26
+ return array ? [] : {};
27
+ } // Returns true or false if a subject has iterable keys or not
28
+
29
+
30
+ function hasKeys(subj) {
31
+ return isArray(subj) || Object.getPrototypeOf(subj) === Object.prototype;
32
+ } // Returns true if the provided key looks like an array key
33
+
34
+
35
+ const ARRAY_PATH_KEY_REG = /^(\[\d+]|0|[1-9]\d*)$/;
36
+
37
+ function isArrayKey(key) {
38
+ return isInteger(key) || ARRAY_PATH_KEY_REG.test(key);
39
+ } // Split a property path string by dot or array notation
40
+
41
+
42
+ function parsePropertyPath(path) {
43
+ return isArray(path) ? path : path.split('.').reduce((full, part) => {
44
+ return full.concat(part.split('[').reduce((f, p) => {
45
+ if (p.endsWith(']')) p = p.slice(0, -1);
46
+ return f.concat(isArrayKey(p) ? parseInt(p, 10) : p);
47
+ }, []));
48
+ }, []);
49
+ } // Join an array of path parts into a single path string
50
+
51
+
52
+ function joinPropertyPath(path) {
53
+ if (typeof path === 'string') return path;
54
+ let joined = path.map(k => isArrayKey(k) ? `[${k}]` : `.${k}`).join('');
55
+ if (joined.startsWith('.')) return joined.substr(1);
56
+ return joined;
57
+ } // Gets a value in the object at the path
58
+
59
+
60
+ function get(object, path, find) {
61
+ return parsePropertyPath(path).reduce((target, key) => target === null || target === void 0 ? void 0 : target[key], object);
62
+ } // Sets a value to the object at the path creating any necessary nested
63
+ // objects or arrays along the way
64
+
65
+
66
+ function set(object, path, value) {
67
+ return parsePropertyPath(path).reduce((target, key, index, path) => {
68
+ if (index < path.length - 1) {
69
+ var _target$key;
70
+
71
+ (_target$key = target[key]) !== null && _target$key !== void 0 ? _target$key : target[key] = create(isArrayKey(path[index + 1]));
72
+ return target[key];
73
+ } else {
74
+ target[key] = value;
75
+ return object;
76
+ }
77
+ }, object);
78
+ } // Deletes properties from an object at the paths
79
+
80
+
81
+ function del(object, ...paths) {
82
+ return paths.reduce((object, path) => {
83
+ return parsePropertyPath(path).reduce((target, key, index, path) => {
84
+ if (index < path.length - 1) {
85
+ return target === null || target === void 0 ? void 0 : target[key];
86
+ } else {
87
+ target === null || target === void 0 ? true : delete target[key];
88
+ return object;
89
+ }
90
+ }, object);
91
+ }, object);
92
+ } // Maps a value from one path to another, deleting the first path
93
+
94
+
95
+ function map(object, from, to, transform = v => v) {
96
+ return set(object, to, transform(parsePropertyPath(from).reduce((target, key, index, path) => {
97
+ let value = target === null || target === void 0 ? void 0 : target[key];
98
+
99
+ if (index === path.length - 1) {
100
+ target === null || target === void 0 ? true : delete target[key];
101
+ }
102
+
103
+ return value;
104
+ }, object)));
105
+ } // Steps through an object's properties calling the function with the path and value of each
106
+
107
+
108
+ function walk(object, fn, path = []) {
109
+ if (path.length && fn([...path], object) === false) return;
110
+
111
+ if (object != null && typeof object === 'object') {
112
+ let isArrayObject = isArray(object);
113
+
114
+ for (let [key, value] of entries(object)) {
115
+ if (isArrayObject) key = parseInt(key, 10);
116
+ walk(value, fn, [...path, key]);
117
+ }
118
+ }
119
+ } // Merges source values and returns a new merged value. The map function will be called with a
120
+ // property's path, previous value, and next value; it should return an array containing any
121
+ // replacement path and value; when a replacement value not defined, values will be merged.
122
+
123
+
124
+ function merge(sources, map) {
125
+ return sources.reduce((target, source, i) => {
126
+ let isSourceArray = isArray(source);
127
+ walk(source, (path, value) => {
128
+ var _ctx;
129
+
130
+ let ctx = get(target, path.slice(0, -1));
131
+ let key = path[path.length - 1];
132
+ let prev = (_ctx = ctx) === null || _ctx === void 0 ? void 0 : _ctx[key]; // maybe map the property path and/or value
133
+
134
+ let [mapped, next] = (map === null || map === void 0 ? void 0 : map(path, prev, value)) || []; // update the context and path if changed
135
+
136
+ if (mapped !== null && mapped !== void 0 && mapped.some((m, i) => m !== path[i])) {
137
+ ctx = get(target, mapped.slice(0, -1));
138
+ path = [...mapped];
139
+ } // adjust path to concat array values when necessary
140
+
141
+
142
+ if (next !== null && (isArray(ctx) || isInteger(key))) {
143
+ var _ctx$length, _ctx2;
144
+
145
+ path.splice(-1, 1, (_ctx$length = (_ctx2 = ctx) === null || _ctx2 === void 0 ? void 0 : _ctx2.length) !== null && _ctx$length !== void 0 ? _ctx$length : 0);
146
+ } // delete prev values
147
+
148
+
149
+ if (next === null || next == null && value === null) {
150
+ del(target, path);
151
+ } // set the next or default value if there is one
152
+
153
+
154
+ if (next != null || next !== null && value != null && !hasKeys(value)) {
155
+ var _target;
156
+
157
+ set((_target = target) !== null && _target !== void 0 ? _target : target = create(isSourceArray), path, next !== null && next !== void 0 ? next : value);
158
+ } // do not recurse mapped objects
159
+
160
+
161
+ return next === undefined;
162
+ });
163
+ return target;
164
+ }, undefined);
165
+ } // Recursively mutate and filter empty values from arrays and objects
166
+
167
+
168
+ function filterEmpty(subject) {
169
+ if (typeof subject === 'object') {
170
+ if (isArray(subject)) {
171
+ for (let i = 0; i < subject.length; i++) {
172
+ if (!filterEmpty(subject[i])) {
173
+ subject.splice(i--, 1);
174
+ }
175
+ }
176
+
177
+ return subject.length > 0;
178
+ } else {
179
+ for (let k in subject) {
180
+ if (!filterEmpty(subject[k])) {
181
+ delete subject[k];
182
+ }
183
+ }
184
+
185
+ return entries(subject).length > 0;
186
+ }
187
+ } else {
188
+ return subject != null;
189
+ }
190
+ }
package/dist/validate.js CHANGED
@@ -3,32 +3,77 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.getSchema = getSchema;
7
6
  exports.addSchema = addSchema;
7
+ exports.default = void 0;
8
+ exports.getSchema = getSchema;
8
9
  exports.resetSchema = resetSchema;
9
- exports.default = validate;
10
+ exports.validate = validate;
10
11
 
11
12
  var _ajv = _interopRequireDefault(require("ajv"));
12
13
 
13
- var _logger = _interopRequireDefault(require("@percy/logger"));
14
+ var _utils = require("./utils");
14
15
 
15
16
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
17
 
18
+ const {
19
+ isArray
20
+ } = Array;
17
21
  const {
18
22
  assign,
19
23
  entries
20
- } = Object; // Ajv manages and validates schemas.
24
+ } = Object; // AJV manages and validates schemas.
21
25
 
22
26
  const ajv = new _ajv.default({
27
+ strict: false,
23
28
  verbose: true,
24
29
  allErrors: true,
25
- schemas: {
26
- config: getDefaultSchema()
27
- }
30
+ schemas: [getDefaultSchema()],
31
+ keywords: [{
32
+ // custom instanceof schema validation
33
+ keyword: 'instanceof',
34
+ metaSchema: {
35
+ enum: ['Function', 'RegExp']
36
+ },
37
+ error: {
38
+ message: cxt => _ajv.default.str`must be an instanceof ${cxt.schemaCode}`,
39
+ params: cxt => _ajv.default._`{ instanceof: ${cxt.schemaCode} }`
40
+ },
41
+ code: cxt => cxt.fail(_ajv.default._`!(${cxt.data} instanceof ${_ajv.default._([cxt.schema])})`)
42
+ }, {
43
+ // disallowed validation based on required
44
+ keyword: 'disallowed',
45
+ metaSchema: {
46
+ type: 'array',
47
+ items: {
48
+ type: 'string'
49
+ }
50
+ },
51
+ error: {
52
+ message: 'disallowed property',
53
+ params: cxt => _ajv.default._`{ disallowedProperty: ${cxt.params.disallowedProperty} }`
54
+ },
55
+ code: cxt => {
56
+ let {
57
+ data,
58
+ gen,
59
+ schema
60
+ } = cxt;
61
+
62
+ for (let prop of schema) {
63
+ gen.if(_ajv.default._`${data}.${_ajv.default._([prop])} !== undefined`, () => {
64
+ cxt.setParams({
65
+ disallowedProperty: _ajv.default._`${prop}`
66
+ }, true);
67
+ cxt.error();
68
+ });
69
+ }
70
+ }
71
+ }]
28
72
  }); // Returns a new default schema.
29
73
 
30
74
  function getDefaultSchema() {
31
75
  return {
76
+ $id: '/config',
32
77
  type: 'object',
33
78
  additionalProperties: false,
34
79
  properties: {
@@ -38,84 +83,174 @@ function getDefaultSchema() {
38
83
  }
39
84
  }
40
85
  };
41
- } // Gets the schema object from the AJV schema.
86
+ } // Gets the schema object from the AJV schema. If a path is provided, an array of schemas is
87
+ // returned, with each index representing the schema of each part of the path (index zero is root).
88
+
42
89
 
90
+ function getSchema(name, path, root) {
91
+ var _ajv$getSchema, _schema, _schema2, _schema3, _schema4;
92
+
93
+ // get the root schema if necessary, resolve it, and return it when there is no path
94
+ let schema = typeof name === 'string' ? (_ajv$getSchema = ajv.getSchema(name)) === null || _ajv$getSchema === void 0 ? void 0 : _ajv$getSchema.schema : name;
95
+
96
+ if ((_schema = schema) !== null && _schema !== void 0 && _schema.$ref) {
97
+ let [name, ref = ''] = schema.$ref.split('#');
98
+ schema = (0, _utils.get)(getSchema(name || root), ref.split('/').slice(1));
99
+ }
43
100
 
44
- function getSchema(name) {
45
- return ajv.getSchema(name).schema;
46
- } // Adds schemas to the config schema's properties. The config schema is removed,
47
- // modified, and replaced after the new schemas are added to clear any compiled
48
- // caches. Existing schemas are removed and replaced as well.
101
+ if (!path) return schema; // destructure the path and set the default root schema for future refs
102
+
103
+ let [key, ...rest] = path = (0, _utils.parsePropertyPath)(path);
104
+ root || (root = schema); // if the desired schema is one of many, we need to find one that best matches
105
+
106
+ let many = isArray(schema) ? schema : (_schema2 = schema) === null || _schema2 === void 0 ? void 0 : _schema2[['anyOf', 'oneOf', 'allOf'].find(p => schema[p])];
107
+
108
+ if (many) {
109
+ let isLooseMatch = s => (s === null || s === void 0 ? void 0 : s.type) === 'object' && s.additionalProperties !== false; // the best possible match will match most of the path or loosely match
110
+
111
+
112
+ return many.map(p => getSchema(p, path, root)).sort((a, b) => b.length - a.length || (isLooseMatch(a[0]) ? -1 : 1))[0];
113
+ } else if ((0, _utils.isArrayKey)(key) && ((_schema3 = schema) === null || _schema3 === void 0 ? void 0 : _schema3.type) === 'array') {
114
+ // find the remaining paths in the items schema
115
+ return [schema].concat(getSchema(schema.items, rest, root));
116
+ } else if (path.length && ((_schema4 = schema) === null || _schema4 === void 0 ? void 0 : _schema4.type) === 'object') {
117
+ var _schema$properties;
118
+
119
+ // find the remaining paths nested in the property schema
120
+ return [schema].concat(getSchema((_schema$properties = schema.properties) === null || _schema$properties === void 0 ? void 0 : _schema$properties[key], rest, root));
121
+ } else if (!path.length && schema) {
122
+ // end of path matching
123
+ return [schema];
124
+ } else {
125
+ // no match
126
+ return [];
127
+ }
128
+ } // Adds schemas to the config schema's properties. The config schema is removed, modified, and
129
+ // replaced after the new schemas are added to clear any compiled caches. Existing schemas are
130
+ // removed and replaced as well. If a schema has an existing $id, the schema will not be added
131
+ // as config schema properties.
49
132
 
50
133
 
51
134
  function addSchema(schemas) {
52
- let config = getSchema('config');
53
- ajv.removeSchema('config');
135
+ if (isArray(schemas)) {
136
+ return schemas.map(addSchema);
137
+ }
138
+
139
+ if (schemas.$id) {
140
+ let {
141
+ $id
142
+ } = schemas;
143
+ if (ajv.getSchema($id)) ajv.removeSchema($id);
144
+ return ajv.addSchema(schemas);
145
+ }
146
+
147
+ let config = getSchema('/config');
148
+ ajv.removeSchema('/config');
54
149
 
55
- for (let [$id, schema] of entries(schemas)) {
150
+ for (let [key, schema] of entries(schemas)) {
151
+ let $id = `/config/${key}`;
56
152
  if (ajv.getSchema($id)) ajv.removeSchema($id);
57
153
  assign(config.properties, {
58
- [$id]: {
154
+ [key]: {
59
155
  $ref: $id
60
156
  }
61
157
  });
62
158
  ajv.addSchema(schema, $id);
63
159
  }
64
160
 
65
- ajv.addSchema(config, 'config');
161
+ return ajv.addSchema(config, '/config');
66
162
  } // Resets the schema by removing all schemas and inserting a new default schema.
67
163
 
68
164
 
69
165
  function resetSchema() {
70
166
  ajv.removeSchema();
71
- ajv.addSchema(getDefaultSchema(), 'config');
72
- } // Validates config data according to the config schema and logs warnings to the
73
- // console. Optionallly scrubs invalid values from the provided config. Returns
74
- // true when the validation success, false otherwise.
167
+ ajv.addSchema(getDefaultSchema(), '/config');
168
+ } // Adds "a" or "an" to a word for readability.
169
+
170
+
171
+ function a(word) {
172
+ if (word === 'undefined' || word === 'null') return word;
173
+ return `${'aeiou'.includes(word[0]) ? 'an' : 'a'} ${word}`;
174
+ } // Default errors anywhere within these keywords can be confusing
175
+
75
176
 
177
+ const HIDE_NESTED_KEYWORDS = ['oneOf', 'anyOf', 'allOf', 'not'];
76
178
 
77
- function validate(config, {
78
- scrub
79
- } = {}) {
80
- let result = ajv.validate('config', config);
179
+ function shouldHideError({
180
+ parentSchema,
181
+ keyword,
182
+ schemaPath
183
+ }) {
184
+ var _parentSchema$errors;
81
185
 
82
- if (!result) {
83
- _logger.default.warn('Invalid config:');
186
+ return !(parentSchema.error || (_parentSchema$errors = parentSchema.errors) !== null && _parentSchema$errors !== void 0 && _parentSchema$errors[keyword]) && HIDE_NESTED_KEYWORDS.some(k => schemaPath.includes(`/${k}`));
187
+ } // Validates data according to the associated schema and returns a list of errors, if any.
188
+
189
+
190
+ function validate(data, key = '/config') {
191
+ if (!ajv.validate(key, data)) {
192
+ let errors = new Map();
84
193
 
85
194
  for (let error of ajv.errors) {
195
+ var _parentSchema$errors2;
196
+
197
+ if (shouldHideError(error)) continue;
86
198
  let {
87
- dataPath,
199
+ instancePath,
200
+ parentSchema,
88
201
  keyword,
89
- params,
90
202
  message,
91
- data
203
+ params
92
204
  } = error;
93
- let pre = dataPath ? `'${dataPath.substr(1)}' ` : '';
205
+ let path = instancePath ? instancePath.substr(1).split('/') : []; // generate a custom error message
94
206
 
95
- if (keyword === 'required') {
96
- message = `is missing required property '${params.missingProperty}'`;
97
- } else if (keyword === 'additionalProperties') {
98
- pre = pre ? `${pre}has ` : '';
99
- message = `unknown property '${params.additionalProperty}'`;
100
- if (scrub) delete data[params.additionalProperty];
207
+ if (parentSchema.error || (_parentSchema$errors2 = parentSchema.errors) !== null && _parentSchema$errors2 !== void 0 && _parentSchema$errors2[keyword]) {
208
+ let custom = parentSchema.error || parentSchema.errors[keyword];
209
+ message = typeof custom === 'function' ? custom(error) : custom;
101
210
  } else if (keyword === 'type') {
102
- let dataType = Array.isArray(data) ? 'array' : typeof data;
103
- message = `should be ${a(params.type)}, received ${a(dataType)}`;
211
+ let dataType = error.data === null ? 'null' : isArray(error.data) ? 'array' : typeof error.data;
212
+ message = `must be ${a(params.type)}, received ${a(dataType)}`;
213
+ } else if (keyword === 'required') {
214
+ message = 'missing required property';
215
+ } else if (keyword === 'additionalProperties') {
216
+ message = 'unknown property';
217
+ } // fix paths
104
218
 
105
- if (scrub) {
106
- let [key, ...path] = dataPath.substr(1).split('.').reverse();
107
- delete path.reduceRight((d, k) => d[k], config)[key];
108
- }
109
- }
110
219
 
111
- _logger.default.warn(`- ${pre}${message}`);
112
- }
113
- }
220
+ if (params.missingProperty) {
221
+ path.push(params.missingProperty);
222
+ } else if (params.additionalProperty) {
223
+ path.push(params.additionalProperty);
224
+ } else if (params.disallowedProperty) {
225
+ path.push(params.disallowedProperty);
226
+ } // fix invalid data
114
227
 
115
- return result;
116
- } // Adds "a" or "an" to a word for readability.
117
228
 
229
+ if (keyword === 'minimum') {
230
+ (0, _utils.set)(data, path, Math.max(error.data, error.schema));
231
+ } else if (keyword === 'maximum') {
232
+ (0, _utils.set)(data, path, Math.min(error.data, error.schema));
233
+ } else if (keyword === 'required') {
234
+ (0, _utils.del)(data, path.slice(0, -1));
235
+ } else {
236
+ (0, _utils.del)(data, path);
237
+ } // joined for error messages
118
238
 
119
- function a(word) {
120
- return `${'aeiou'.includes(word[0]) ? 'an' : 'a'} ${word}`;
121
- }
239
+
240
+ path = (0, _utils.joinPropertyPath)(path); // map one error per path
241
+
242
+ errors.set(path, {
243
+ path,
244
+ message
245
+ });
246
+ } // filter empty values as a result of scrubbing
247
+
248
+
249
+ (0, _utils.filterEmpty)(data); // return an array of errors
250
+
251
+ return Array.from(errors.values());
252
+ }
253
+ }
254
+
255
+ var _default = validate;
256
+ exports.default = _default;
package/package.json CHANGED
@@ -1,36 +1,39 @@
1
1
  {
2
2
  "name": "@percy/config",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.73",
4
4
  "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/percy/cli",
8
+ "directory": "packages/config"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
5
13
  "main": "dist/index.js",
6
14
  "types": "types/index.d.ts",
7
15
  "files": [
8
16
  "dist",
9
17
  "types/index.d.ts"
10
18
  ],
19
+ "engines": {
20
+ "node": ">=12"
21
+ },
11
22
  "scripts": {
12
- "build": "babel --root-mode upward src --out-dir dist",
23
+ "build": "node ../../scripts/build",
13
24
  "lint": "eslint --ignore-path ../../.gitignore .",
14
- "test": "cross-env NODE_ENV=test mocha",
15
- "test:coverage": "nyc yarn test",
25
+ "test": "node ../../scripts/test",
26
+ "test:coverage": "yarn test --coverage",
16
27
  "test:types": "tsd"
17
28
  },
18
- "publishConfig": {
19
- "access": "public"
20
- },
21
- "mocha": {
22
- "require": "../../scripts/babel-register"
23
- },
24
29
  "dependencies": {
25
- "@percy/logger": "^1.0.0-beta.7",
26
- "ajv": "^6.12.0",
30
+ "@percy/logger": "1.0.0-beta.73",
31
+ "ajv": "^8.6.2",
27
32
  "cosmiconfig": "^7.0.0",
28
- "deepmerge": "^4.2.2",
29
- "path-type": "^4.0.0",
30
- "yaml": "^1.8.0"
33
+ "yaml": "^1.10.0"
31
34
  },
32
35
  "devDependencies": {
33
36
  "json-schema-typed": "^7.0.3"
34
37
  },
35
- "gitHead": "5be796ec8f17958e93ada0b634899b945c9b0d60"
38
+ "gitHead": "aa8160e02bea3e04ab1d3605762f89fbe79605d4"
36
39
  }