diff --git a/README.md b/README.md index 3a570b1..565ccc0 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,14 @@ log('debug log') Create a log entry with a red cross. ```typescript -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - error log * @module log/docs/examples/error */ -log('error log', { level: 'ERROR' }) +log('error log', { level: LogLevel.ERROR }) ``` #### Info @@ -68,14 +68,14 @@ log('error log', { level: 'ERROR' }) Create a log entry with a blue info symbol. ```typescript -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - info log * @module log/docs/examples/info */ -log('info log', { level: 'INFO' }) +log('info log', { level: LogLevel.INFO }) ``` #### Success @@ -83,14 +83,14 @@ log('info log', { level: 'INFO' }) Create a log entry with a green tick mark. ```typescript -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - success log * @module log/docs/examples/success */ -log('success log', { level: 'SUCCESS' }) +log('success log', { level: LogLevel.SUCCESS }) ``` #### Warning @@ -98,20 +98,20 @@ log('success log', { level: 'SUCCESS' }) Create a log entry with a yellow exclamation point. ```typescript -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - warning log * @module log/docs/examples/warning */ -log('warning log', { level: 'WARN' }) +log('warning log', { level: LogLevel.WARN }) ``` ### Options ```typescript -export interface LogOptions { +interface LogOptions { /** * Log arguments. * @@ -159,7 +159,15 @@ export interface LogOptions { silent?: boolean } -type Level = 'DEBUG' | 'ERROR' | 'INFO' | 'SUCCESS' | 'WARN' +enum LogLevel { + DEBUG = 'debug', + ERROR = 'error', + INFO = 'info', + SUCCESS = 'success', + WARN = 'warn' +} + +export type Level = keyof typeof LogLevel | LogLevel ``` ## Built With diff --git a/docs/examples/error.ts b/docs/examples/error.ts index 6cd1032..26d3317 100644 --- a/docs/examples/error.ts +++ b/docs/examples/error.ts @@ -1,8 +1,8 @@ -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - error log * @module log/docs/examples/error */ -log('error log', { level: 'ERROR' }) +log('error log', { level: LogLevel.ERROR }) diff --git a/docs/examples/info.ts b/docs/examples/info.ts index f4e4300..a4489cf 100644 --- a/docs/examples/info.ts +++ b/docs/examples/info.ts @@ -1,8 +1,8 @@ -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - info log * @module log/docs/examples/info */ -log('info log', { level: 'INFO' }) +log('info log', { level: LogLevel.INFO }) diff --git a/docs/examples/success.ts b/docs/examples/success.ts index e694476..9ca58d3 100644 --- a/docs/examples/success.ts +++ b/docs/examples/success.ts @@ -1,8 +1,8 @@ -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - success log * @module log/docs/examples/success */ -log('success log', { level: 'SUCCESS' }) +log('success log', { level: LogLevel.SUCCESS }) diff --git a/docs/examples/warning.ts b/docs/examples/warning.ts index 2f07ed2..17e4646 100644 --- a/docs/examples/warning.ts +++ b/docs/examples/warning.ts @@ -1,8 +1,8 @@ -import log from '@flex-development/log' +import log, { LogLevel } from '@flex-development/log' /** * @file Examples - warning log * @module log/docs/examples/warning */ -log('warning log', { level: 'WARN' }) +log('warning log', { level: LogLevel.WARN }) diff --git a/src/__tests__/log.functional.spec.ts b/src/__tests__/log.functional.spec.ts index 2e83670..524197d 100644 --- a/src/__tests__/log.functional.spec.ts +++ b/src/__tests__/log.functional.spec.ts @@ -1,9 +1,9 @@ import defaults from '@log/config/defaults.config' import type { LogOptions } from '@log/interfaces' import format from '@log/utils/format.util' +import normalizeOptions from '@log/utils/normalize-options.util' import type { RestoreConsole } from 'jest-mock-console' import mockConsole from 'jest-mock-console' -import merge from 'lodash.merge' import sh from 'shelljs' import testSubject from '../log' @@ -13,10 +13,13 @@ import testSubject from '../log' */ jest.mock('@log/utils/format.util') +jest.mock('@log/utils/normalize-options.util') const mockSH = sh as jest.Mocked const mockFormat = format as jest.MockedFunction -const mockMerge = merge as jest.MockedFunction +const mockNormalizeOptions = normalizeOptions as jest.MockedFunction< + typeof normalizeOptions +> describe('functional:log', () => { const restoreConsole: RestoreConsole = mockConsole(['log']) @@ -24,7 +27,7 @@ describe('functional:log', () => { afterAll(() => restoreConsole()) - it('should merge options with defaults', () => { + it('should normalize options', () => { // Arrange const options: LogOptions = { level: 'ERROR' } @@ -32,9 +35,7 @@ describe('functional:log', () => { testSubject('', options) // Expect - expect(mockMerge).toBeCalledTimes(1) - expect(mockMerge.mock.calls[0][1]).toStrictEqual(defaults) - expect(mockMerge.mock.calls[0][2]).toStrictEqual(options) + expect(mockNormalizeOptions.mock.calls[0][0]).toMatchObject(options) }) it('should format log entry', () => { @@ -47,7 +48,7 @@ describe('functional:log', () => { // Expect expect(mockFormat).toBeCalledTimes(1) - expect(mockFormat).toBeCalledWith(data, options) + expect(mockFormat).toBeCalledWith(data, mockNormalizeOptions(options)) }) describe('options', () => { diff --git a/src/config/defaults.config.ts b/src/config/defaults.config.ts index b89990e..b211645 100644 --- a/src/config/defaults.config.ts +++ b/src/config/defaults.config.ts @@ -1,3 +1,4 @@ +import { LogLevel } from '@log/enums/log-level.enum' import type { LogOptions } from '@log/interfaces' /** @@ -9,5 +10,5 @@ export default Object.freeze({ args: [], bold: { args: true, data: false }, color: {}, - level: 'DEBUG' + level: LogLevel.DEBUG }) as Readonly>> diff --git a/src/enums/log-figure.enum.ts b/src/enums/log-figure.enum.ts index 78d5a0f..2fefc22 100644 --- a/src/enums/log-figure.enum.ts +++ b/src/enums/log-figure.enum.ts @@ -1,5 +1,5 @@ import figures from 'figures' -import type { Level } from './log-level.enum' +import type { LogLevel } from './log-level.enum' /** * @file Enums - LogFigure @@ -14,10 +14,11 @@ import type { Level } from './log-level.enum' * @readonly * @enum {string} */ -export const LogFigure: Readonly> = Object.freeze({ - DEBUG: '', - ERROR: figures.cross, - INFO: figures.info, - SUCCESS: figures.tick, - WARN: '!' -}) +export const LogFigure: Readonly> = + Object.freeze({ + DEBUG: '', + ERROR: figures.cross, + INFO: figures.info, + SUCCESS: figures.tick, + WARN: '!' + }) diff --git a/src/enums/log-level.enum.ts b/src/enums/log-level.enum.ts index 93409ce..1927cb0 100644 --- a/src/enums/log-level.enum.ts +++ b/src/enums/log-level.enum.ts @@ -11,7 +11,5 @@ export enum LogLevel { ERROR = 'error', INFO = 'info', SUCCESS = 'success', - WARN = 'warning' + WARN = 'warn' } - -export type Level = keyof typeof LogLevel diff --git a/src/index.ts b/src/index.ts index 96a0105..fec068e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,8 @@ import log from './log' * @module log */ +export { LogLevel } from './enums/log-level.enum' +export type { LogOptions } from './interfaces' +export type { Level } from './types' + export default log diff --git a/src/interfaces/log-options.interface.ts b/src/interfaces/log-options.interface.ts index 0107909..6cb9d37 100644 --- a/src/interfaces/log-options.interface.ts +++ b/src/interfaces/log-options.interface.ts @@ -1,5 +1,5 @@ import type { NullishString } from '@flex-development/tutils' -import type { Level } from '@log/enums/log-level.enum' +import type { Level } from '@log/types' import figures from 'figures' import type { LogOptionsBold } from './log-options-bold.interface' import type { LogOptionsColor } from './log-options-color.interface' @@ -42,7 +42,7 @@ export interface LogOptions { /** * Log level. * - * @default 'DEBUG' + * @default LogLevel.DEBUG */ level?: Level diff --git a/src/log.ts b/src/log.ts index c218bb3..e1fc5c1 100644 --- a/src/log.ts +++ b/src/log.ts @@ -1,15 +1,15 @@ import type { NullishString } from '@flex-development/tutils' import defaults from '@log/config/defaults.config' -import type { Level } from '@log/enums/log-level.enum' import type { LogOptions, LogOptionsBold, LogOptionsColor } from '@log/interfaces' +import type { Level } from '@log/types' import type { Color } from 'chalk' import figs from 'figures' -import merge from 'lodash.merge' import format from './utils/format.util' +import normalizeOptions from './utils/normalize-options.util' /** * @file Log Method @@ -34,14 +34,14 @@ import format from './utils/format.util' * @param {typeof Color} [options.color.data] - Set log data color * @param {typeof Color} [options.color.figure] - Set log figure color * @param {keyof typeof figs | NullishString} [options.figure] - Override figure - * @param {Level} [options.level='DEBUG'] - Log level + * @param {Level} [options.level=LogLevel.DEBUG] - Log level * @param {boolean} [options.shell] - Use [`echo`][2] instead of `console.log` * @param {boolean} [options.silent] - Do not log any output * @return {string} Formatted log entry */ const log = (data: any, options: LogOptions = defaults): string => { // Merge options with defaults - const $options = merge({}, defaults, options) + const $options = normalizeOptions(options) // Create formatted log entry const entry = format(data, $options) diff --git a/src/types/Level.ts b/src/types/Level.ts new file mode 100644 index 0000000..cff324c --- /dev/null +++ b/src/types/Level.ts @@ -0,0 +1,11 @@ +import type { LogLevel } from '@log/enums/log-level.enum' + +/** + * @file Type Definitions - Level + * @module log/types/Level + */ + +/** + * Log levels. + */ +export type Level = keyof typeof LogLevel | LogLevel diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..ef3649e --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,6 @@ +/** + * @file Entry Point - Type Definitions + * @module log/types + */ + +export type { Level } from './Level' diff --git a/src/utils/__mocks__/normalize-options.util.ts b/src/utils/__mocks__/normalize-options.util.ts new file mode 100644 index 0000000..f31eb6f --- /dev/null +++ b/src/utils/__mocks__/normalize-options.util.ts @@ -0,0 +1,9 @@ +/** + * @file User Module Mock - normalizeOptions + * @module utils/mocks/normalizeOptions + * @see https://jestjs.io/docs/manual-mocks#mocking-user-modules + */ + +export default jest.fn((...args) => { + return jest.requireActual('../normalize-options.util').default(...args) +}) diff --git a/src/utils/__tests__/figure.util.spec.ts b/src/utils/__tests__/figure.util.spec.ts index eab8586..2fc950d 100644 --- a/src/utils/__tests__/figure.util.spec.ts +++ b/src/utils/__tests__/figure.util.spec.ts @@ -1,14 +1,16 @@ import defaults from '@log/config/defaults.config' import { LogColor } from '@log/enums/log-color.enum' import { LogFigure } from '@log/enums/log-figure.enum' +import { LogLevel } from '@log/enums/log-level.enum' import type { LogOptions } from '@log/interfaces' +import normalizeOptions from '@log/utils/normalize-options.util' import type { Testcase } from '@tests/utils/types' import ch from 'chalk' import testSubject from '../figure.util' /** * @file Unit Tests - figure - * @module log/utils/figure + * @module log/utils/tests/unit/figure */ const mockCH = ch as jest.Mocked @@ -25,7 +27,7 @@ describe('unit:utils/figure', () => { expected: '', options: defaults, result: 'return empty string', - state: `options.level === 'DEBUG' && !options.figure` + state: `options.level === '${LogLevel.DEBUG}' && !options.figure` }, { expected: '', @@ -33,30 +35,66 @@ describe('unit:utils/figure', () => { result: 'return empty string', state: 'options.figure === null' }, + { + expected: '', + options: { figure: '' }, + result: 'return empty string', + state: 'options.figure is empty string (trimmed)' + }, + { + expected: '', + options: { figure: ' ' }, + result: 'return empty string', + state: 'options.figure is empty string (untrimmed)' + }, { expected: '?', options: { figure: '?' }, result: 'return string matching ?', state: 'options.figure is non-empty string' }, + { + expected: LogFigure.ERROR, + options: { level: LogLevel.ERROR }, + result: `return string matching ${LogFigure.ERROR}`, + state: `options.level === '${LogLevel.ERROR}'` + }, { expected: LogFigure.ERROR, options: { level: 'ERROR' }, result: `return string matching ${LogFigure.ERROR}`, state: `options.level === 'ERROR'` }, + { + expected: LogFigure.INFO, + options: { level: LogLevel.INFO }, + result: `return string matching ${LogFigure.INFO}`, + state: `options.level === '${LogLevel.INFO}'` + }, { expected: LogFigure.INFO, options: { level: 'INFO' }, result: `return string matching ${LogFigure.INFO}`, state: `options.level === 'INFO'` }, + { + expected: LogFigure.SUCCESS, + options: { level: LogLevel.SUCCESS }, + result: `return string matching ${LogFigure.SUCCESS}`, + state: `options.level === '${LogLevel.SUCCESS}'` + }, { expected: LogFigure.SUCCESS, options: { level: 'SUCCESS' }, result: `return string matching ${LogFigure.SUCCESS}`, state: `options.level === 'SUCCESS'` }, + { + expected: LogFigure.WARN, + options: { level: LogLevel.WARN }, + result: `return string matching ${LogFigure.WARN}`, + state: `options.level === '${LogLevel.WARN}'` + }, { expected: LogFigure.WARN, options: { level: 'WARN' }, @@ -67,11 +105,11 @@ describe('unit:utils/figure', () => { const name = 'should $result if $state' - it.each(cases)(name, testcase => { + it.each(cases)(name, ({ expected, options }) => { // Arrange - const { expected, options } = testcase - const { color = defaults.color, figure, level = defaults.level } = options - const spy_ch_bold_method = color?.figure ?? LogColor[level] + options = normalizeOptions(options) + const { color = defaults.color, figure, level = 'DEBUG' } = options + const spy_ch_bold_method = color.figure ?? LogColor[level] const spy_ch_bold = jest.spyOn(mockCH.bold, spy_ch_bold_method) // Act diff --git a/src/utils/__tests__/format.util.functional.spec.ts b/src/utils/__tests__/format.util.functional.spec.ts index 1d5004f..04469b2 100644 --- a/src/utils/__tests__/format.util.functional.spec.ts +++ b/src/utils/__tests__/format.util.functional.spec.ts @@ -1,6 +1,9 @@ import defaults from '@log/config/defaults.config' +import { LogLevel } from '@log/enums/log-level.enum' import type { LogOptions } from '@log/interfaces' +import type { Level } from '@log/types' import figure from '@log/utils/figure.util' +import normalizeOptions from '@log/utils/normalize-options.util' import type { TestcaseCalled } from '@tests/utils/types' import type { Color } from 'chalk' import ch from 'chalk' @@ -9,13 +12,17 @@ import testSubject from '../format.util' /** * @file Functional Tests - format - * @module log/utils/tests/format + * @module log/utils/tests/functional/format */ jest.mock('@log/utils/figure.util') +jest.mock('@log/utils/normalize-options.util') const mockCH = ch as jest.Mocked const mockFigure = figure as jest.MockedFunction +const mockNormalizeOptions = normalizeOptions as jest.MockedFunction< + typeof normalizeOptions +> describe('functional:utils/format', () => { type CaseCalled = Omit & { @@ -41,7 +48,7 @@ describe('functional:utils/format', () => { it('should add log figure', () => { expect(mockFigure).toBeCalledTimes(1) - expect(mockFigure).toBeCalledWith(defaults) + expect(mockFigure).toBeCalledWith(mockNormalizeOptions(defaults)) }) it('should format log arguments', () => { @@ -86,6 +93,17 @@ describe('functional:utils/format', () => { }) describe('options', () => { + it('should normalize options', () => { + // Arrange + const options = { level: LogLevel.INFO as Level } + + // Act + testSubject(`${options.level} message`, options) + + // Expect + expect(mockNormalizeOptions.mock.calls[0][0]).toMatchObject(options) + }) + describe('options.bold', () => { const cases: CaseCalled[] = [ { @@ -96,7 +114,7 @@ describe('functional:utils/format', () => { result: 'bold log arguments' }, { - calledWith: data_obj.data, + calledWith: mockCH.gray(data_obj.data), data: data_obj.data, expected: 1, options: { args: [], bold: { data: true } }, diff --git a/src/utils/__tests__/normalize-options.util.functional.spec.ts b/src/utils/__tests__/normalize-options.util.functional.spec.ts new file mode 100644 index 0000000..d0d1851 --- /dev/null +++ b/src/utils/__tests__/normalize-options.util.functional.spec.ts @@ -0,0 +1,53 @@ +import defaults from '@log/config/defaults.config' +import { LogLevel } from '@log/enums/log-level.enum' +import type { LogOptions } from '@log/interfaces' +import type { Level } from '@log/types' +import type { Testcase } from '@tests/utils/types' +import merge from 'lodash.merge' +import testSubject from '../normalize-options.util' + +/** + * @file Functional Tests - normalizeOptions + * @module log/utils/tests/functional/normalizeOptions + */ + +const mockMerge = merge as jest.MockedFunction + +describe('functional:utils/normalizeOptions', () => { + it('should merge options with defaults', () => { + // Arrange + const options: LogOptions = { level: 'ERROR' } + + // Act + testSubject(options) + + // Expect + expect(mockMerge).toBeCalledTimes(1) + expect(mockMerge.mock.calls[0]).toMatchObject({}) + expect(mockMerge.mock.calls[0][1]).toMatchObject(defaults) + expect(mockMerge.mock.calls[0][2]).toMatchObject(options) + }) + + describe('options.level', () => { + type Case = Testcase & { level: Level } + + const cases: [Case, Case][] = Object.keys(LogLevel).map(key => { + const level = key as keyof typeof LogLevel + + return [ + { expected: level, level }, + { expected: level, level: LogLevel[level] } + ] + }) + + const name = `should === '$expected' if options.level === '$level'` + + it.each(cases.flat())(name, testcase => { + // Arrange + const { expected, level } = testcase + + // Act + Expect + expect(testSubject({ level }).level).toBe(expected) + }) + }) +}) diff --git a/src/utils/figure.util.ts b/src/utils/figure.util.ts index 06a45ac..f97872e 100644 --- a/src/utils/figure.util.ts +++ b/src/utils/figure.util.ts @@ -2,10 +2,12 @@ import type { NullishString } from '@flex-development/tutils' import defaults from '@log/config/defaults.config' import { LogColor } from '@log/enums/log-color.enum' import { LogFigure } from '@log/enums/log-figure.enum' -import type { Level } from '@log/enums/log-level.enum' +import { LogLevel as LL } from '@log/enums/log-level.enum' import type { LogOptions, LogOptionsColor } from '@log/interfaces' +import type { Level } from '@log/types' import ch from 'chalk' import figs from 'figures' +import normalizeOptions from './normalize-options.util' /** * @file Utility - figure @@ -15,7 +17,8 @@ import figs from 'figures' /** * Selects and colors a log figure. An empty string will be returned if: * - * - `options.level === 'DEBUG' && !options.figure` + * - `options.level.toLowerCase() === 'debug' && !options.figure` + * - `options.figure && options.figure.trim() === ''` * - `options.figure === null` * * The figure and figure color are pre-determined by `options.level`, but can be @@ -24,20 +27,24 @@ import figs from 'figures' * @param {LogOptions} [options=defaults] - `log` options * @param {LogOptionsColor} [options.color={figure:undefined}] - Override color * @param {keyof typeof figs | NullishString} [options.figure] - Override figure - * @param {Level} [options.level='DEBUG'] - Log level + * @param {Level} [options.level=LogLevel.DEBUG] - Log level * @return {string} Colorized log figure or empty string */ const figure = (options: LogOptions = defaults): string => { // Spread options - const { color = defaults.color, figure, level = defaults.level } = options + const { + color, + figure: fig, + level = defaults.level + } = normalizeOptions(options) - // Omit figure - if ((level === 'DEBUG' && !figure) || figure === null) return '' + // Omit if in in debug mode + if (level.toLowerCase() === LL.DEBUG && !fig) return '' - // Select figure - const fig = figure || LogFigure[level] + // Omit if figure is null or an empty string + if (fig === null || fig === '' || (fig && fig.trim() === '')) return '' - return ch.bold[color?.figure ?? LogColor[level]](fig) + return ch.bold[color?.figure ?? LogColor[level]](fig || LogFigure[level]) } export default figure diff --git a/src/utils/format.util.ts b/src/utils/format.util.ts index 78d200a..bca08ad 100644 --- a/src/utils/format.util.ts +++ b/src/utils/format.util.ts @@ -1,17 +1,18 @@ import type { NullishString } from '@flex-development/tutils' import defaults from '@log/config/defaults.config' import { LogColor } from '@log/enums/log-color.enum' -import type { Level } from '@log/enums/log-level.enum' import type { LogOptions, LogOptionsBold, LogOptionsColor } from '@log/interfaces' +import type { Level } from '@log/types' import type { Color } from 'chalk' import ch from 'chalk' import figs from 'figures' import util from 'util' import figure from './figure.util' +import normalizeOptions from './normalize-options.util' /** * @file Utility - format @@ -33,10 +34,13 @@ import figure from './figure.util' * @param {typeof Color} [options.color.data] - Set log data color * @param {typeof Color} [options.color.figure] - Set log figure color * @param {keyof typeof figs | NullishString} [options.figure] - Override figure - * @param {Level} [options.level='DEBUG'] - Log level + * @param {Level} [options.level=LogLevel.DEBUG] - Log level * @return {string} Log data and arguments as string */ const format = (data: any, options: LogOptions = defaults): string => { + // Normalize options + options = normalizeOptions(options) + /** * Returns true if `value` is a function or object. * diff --git a/src/utils/normalize-options.util.ts b/src/utils/normalize-options.util.ts new file mode 100644 index 0000000..f4f1088 --- /dev/null +++ b/src/utils/normalize-options.util.ts @@ -0,0 +1,47 @@ +import type { NullishString } from '@flex-development/tutils' +import defaults from '@log/config/defaults.config' +import type { + LogOptions, + LogOptionsBold, + LogOptionsColor +} from '@log/interfaces' +import type { Level } from '@log/types' +import type { Color } from 'chalk' +import figs from 'figures' +import merge from 'lodash.merge' + +/** + * @file Utility - normalizeOptions + * @module log/utils/normalizeOptions + */ + +/** + * Merges default options and normalizes resulting options. + * + * @param {LogOptions} [options={}] - `log` options + * @param {any[]} [options.args=[]] - Log arguments + * @param {LogOptionsBold} [options.bold={args:true}] - Bold logs + * @param {boolean} [options.bold.args=true] - Bold log arguments + * @param {boolean} [options.bold.data] - Bold log data + * @param {LogOptionsColor} [options.color={args:'white'}] - Override colors + * @param {typeof Color} [options.color.args='white'] - Set log arguments color + * @param {typeof Color} [options.color.data] - Set log data color + * @param {typeof Color} [options.color.figure] - Set log figure color + * @param {keyof typeof figs | NullishString} [options.figure] - Override figure + * @param {Level} [options.level=LogLevel.DEBUG] - Log level + * @param {boolean} [options.shell] - Use [`echo`][2] instead of `console.log` + * @param {boolean} [options.silent] - Do not log any output + * @return {LogOptions} Normalized log level + */ +const normalizeOptions = (options: LogOptions = {}): LogOptions => { + // Merge options with defaults + const opts: LogOptions = merge({}, defaults, options) + + // Normalize options.level + if (opts.level) opts.level = opts.level?.toUpperCase() as LogOptions['level'] + + // Return normalized options + return opts +} + +export default normalizeOptions