diff --git a/packages/cli-config/src/commands/config/migrate.js b/packages/cli-config/src/commands/config/migrate.js index f6802e5ee..950eb1303 100644 --- a/packages/cli-config/src/commands/config/migrate.js +++ b/packages/cli-config/src/commands/config/migrate.js @@ -71,6 +71,13 @@ export class Migrate extends Command { this.log.info('Migrating config file...'); let format = path.extname(output).replace(/^./, '') || 'yaml'; let migrated = PercyConfig.migrate(config); + + // prefer kebab-case for yaml + if (/^ya?ml$/.test(format)) { + migrated = PercyConfig.normalize(migrated, { kebab: true }); + } + + // stringify to the desired format let body = PercyConfig.stringify(format, migrated); // update the package.json entry via string replacement diff --git a/packages/config/src/load.js b/packages/config/src/load.js index b9064ac85..c98187b87 100644 --- a/packages/config/src/load.js +++ b/packages/config/src/load.js @@ -81,7 +81,7 @@ export default function load({ } // merge found config with overrides and validate - config = normalize(config, overrides); + config = normalize(config, { overrides }); let validation = config && validate(config); if (validation && !validation.result) { diff --git a/packages/config/src/normalize.js b/packages/config/src/normalize.js index db527e44d..f17940db9 100644 --- a/packages/config/src/normalize.js +++ b/packages/config/src/normalize.js @@ -2,38 +2,56 @@ const { isArray } = Array; const { entries, assign } = Object; // Edge case camelizations -const CAMELIZE_MAP = { - css: 'CSS', - javascript: 'JavaScript' -}; +const CAMELCASE_MAP = new Map([ + ['css', 'CSS'], + ['javascript', 'JavaScript'] +]); // Converts kebab-cased and snake_cased strings to camelCase. const KEBAB_SNAKE_REG = /[-_]([^-_]+)/g; -function camelize(str) { +function camelcase(str) { return str.replace(KEBAB_SNAKE_REG, (match, word) => ( - CAMELIZE_MAP[word] || (word[0].toUpperCase() + word.slice(1)) + CAMELCASE_MAP.get(word) || (word[0].toUpperCase() + word.slice(1)) )); } +// Coverts camelCased and snake_cased strings to kebab-case. +const CAMEL_SNAKE_REG = /([a-z])([A-Z]+)|_([^_]+)/g; + +function kebabcase(str) { + 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()}` + )); +} + // Merges source values into the target object unless empty. When `options.replaceArrays` is truthy, // target arrays are replaced by their source arrays rather than concatenated together. export function merge(target, source, options) { let isSourceArray = isArray(source); if (options?.replaceArrays && isSourceArray) return source; if (typeof source !== 'object') return source != null ? source : target; + let convertcase = options?.kebab ? kebabcase : camelcase; return entries(source).reduce((result, [key, value]) => { value = merge(result?.[key], value, options); return value == null ? result : isSourceArray ? (result || []).concat(value) - : assign(result || {}, { [camelize(key)]: value }); + : assign(result || {}, { [convertcase(key)]: value }); }, target); } // Recursively reduces config objects and arrays to remove undefined and empty values and rename -// kebab-case properties to camelCase. Optionally allows deep merging of override values -export default function normalize(object, overrides) { - return merge(merge(undefined, object), overrides); +// kebab-case properties to camelCase. Optionally allows deep merging of a second overrides +// argument, and converting keys to kebab-case with a third options.kebab argument. +export default function normalize(object, options) { + object = merge(undefined, object, options); + return options?.overrides + ? merge(object, options.overrides, options) + : object; } diff --git a/packages/config/test/index.test.js b/packages/config/test/index.test.js index 1d01178ff..665ef5fd3 100644 --- a/packages/config/test/index.test.js +++ b/packages/config/test/index.test.js @@ -511,11 +511,31 @@ describe('PercyConfig', () => { expect(PercyConfig.normalize({ 'foo-bar': 'baz', foo: { bar_baz: 'qux' }, - 'foo_bar-baz': 'qux' + 'foo_bar-baz': 'qux', + 'percy-css': '', + 'enable-javascript': false })).toEqual({ fooBar: 'baz', foo: { barBaz: 'qux' }, - fooBarBaz: 'qux' + fooBarBaz: 'qux', + percyCSS: '', + enableJavaScript: false + }); + }); + + it('can converts keys to kebab-case', () => { + expect(PercyConfig.normalize({ + 'foo-bar': 'baz', + foo: { bar_baz: 'qux' }, + fooBar_baz: 'qux', + percyCSS: '', + enableJavaScript: false + }, { kebab: true })).toEqual({ + 'foo-bar': 'baz', + foo: { 'bar-baz': 'qux' }, + 'foo-bar-baz': 'qux', + 'percy-css': '', + 'enable-javascript': false }); }); });