diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dce39d1..118859e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,12 +31,12 @@ jobs: - name: Installing dependencies run: pnpm install --frozen-lockfile - - name: Linting - run: pnpm lint - - name: Type checking run: pnpm typecheck + - name: Linting + run: pnpm lint + - name: Testing run: pnpm coverage diff --git a/example/index.ts b/example/index.ts index 9d927ee..00971ac 100644 --- a/example/index.ts +++ b/example/index.ts @@ -2,6 +2,7 @@ import { randomInt } from 'node:crypto'; import { styleText } from 'node:util'; import { createConsole } from 'styled-json-console'; +import { mergeStyleOptions } from 'styled-json-console/mergeStyleOptions'; const myConsole = createConsole({ inspectOptions: { @@ -37,9 +38,9 @@ const myConsole = createConsole({ // stderr settings are merged with stdout settings so you can override only what you want { space: 2, - style: { - string: ['white', 'whiteBright'], - }, + style: mergeStyleOptions(['whiteBright'], { + number: ['magentaBright'], + }), }, ], }); @@ -50,6 +51,7 @@ const data = { randomNumber: randomInt(0, 2 ** 32 - 1), example: true, json: JSON.stringify({ a: 1, b: [2], c: { d: 3, e: [4, 5] } }), + nested: { a: 1, b: [2], c: { d: 3, e: [4, 5] } }, }; myConsole.info(JSON.stringify(data)); diff --git a/package.json b/package.json index 03f3a65..6d724b4 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,11 @@ "homepage": "https://github.com/webdeveric/styled-json-console/#readme", "packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6", "scripts": { - "clean": "rimraf ./dist/", + "clean": "rimraf ./dist/ ./cache/", "prebuild": "pnpm clean", "build": "tsc --build tsconfig.src.json --force", - "typecheck": "tsc --build tsconfig.src.json tsconfig.test.json tsconfig.configs.json --verbose", - "lint": "eslint ./*.{js,cjs,mjs,ts,cts,mts} ./src/ --ext .ts", + "typecheck": "tsc --build tsconfig.json --verbose", + "lint": "eslint ./*.{js,cjs,mjs,ts,cts,mts} ./src/ ./example/ --ext .ts", "test": "vitest --typecheck", "coverage": "vitest run --coverage --typecheck", "validate": "validate-package-exports --check --verify", diff --git a/src/AnsiJsonWritable.ts b/src/AnsiJsonWritable.ts index 43db442..3d9ad3b 100644 --- a/src/AnsiJsonWritable.ts +++ b/src/AnsiJsonWritable.ts @@ -2,9 +2,9 @@ import { Writable, type WritableOptions } from 'node:stream'; import { ansiHighlightJson } from './ansiHighlightJson.js'; import { isColorEnabled } from './isColorEnabled.js'; -import { Style, type StyleOptions } from './Style.js'; +import { Style } from './Style.js'; -import type { JsonReplacerFn } from './types.js'; +import type { JsonReplacerFn, StyleOptions, StyleTextFormatArray } from './types.js'; export type ModifyOutputFn = (output: string, style: Style) => string; @@ -13,7 +13,7 @@ export type AnsiJsonWritableOptions = WritableOptions & { space?: number | string; eol?: string; replacer?: JsonReplacerFn; - styleOptions?: Partial; + styleOptions?: Partial | StyleTextFormatArray; modifyOutput?: ModifyOutputFn; }; diff --git a/src/Style.ts b/src/Style.ts index 5919590..8b84dec 100644 --- a/src/Style.ts +++ b/src/Style.ts @@ -1,48 +1,14 @@ import { styleText, type StyleTextOptions } from 'node:util'; -import type { StyleTextFormat } from './types.js'; - -export type StyleKeys = - | 'string' - | 'number' - | 'boolean' - | 'bracket' - | 'comma' - | 'colon' - | 'quoteKey' - | 'quoteString' - | 'key' - | 'null'; +import { defaultStyleOptions } from './defaults.js'; +import { mergeStyleOptions } from './mergeStyleOptions.js'; + +import type { StyleKey, StyleOptions, StyleTextFormat, StyleTextFormatArray } from './types.js'; export type StyleFn = (value: string, depth: number) => string; export type BaseStyle = { - [K in StyleKeys]: StyleFn; -}; - -export type StyleOptions = Record< - StyleKeys, - [item: StyleTextFormat, ...rest: StyleTextFormat[]] // At least one item ->; - -export const defaultStyleOptions: StyleOptions = { - // content - string: ['green'], - number: ['yellowBright'], - boolean: ['blueBright'], - null: ['redBright'], - - // structural - bracket: ['white', 'blue', 'yellow', 'cyan', 'green', 'red'], - comma: ['white'], - colon: ['white'], - - // quotes - quoteKey: ['cyan'], - quoteString: ['green'], - - // keys - key: ['cyan'], + [K in StyleKey]: StyleFn; }; export class Style implements BaseStyle { @@ -50,8 +16,8 @@ export class Style implements BaseStyle { readonly #styleTextOptions: StyleTextOptions = { validateStream: false }; - constructor(options?: Partial) { - this.#options = { ...defaultStyleOptions, ...options }; + constructor(options?: Partial | StyleTextFormatArray) { + this.#options = mergeStyleOptions(defaultStyleOptions, options); } #getStyleTextFormat(type: keyof StyleOptions, depth: number): StyleTextFormat { diff --git a/src/createConsole.ts b/src/createConsole.ts index cc58a9b..6f319a0 100644 --- a/src/createConsole.ts +++ b/src/createConsole.ts @@ -1,12 +1,12 @@ import { Console, type ConsoleConstructorOptions } from 'node:console'; import { AnsiJsonWritable, type ModifyOutputFn } from './AnsiJsonWritable.js'; +import { mergeStyleOptions } from './mergeStyleOptions.js'; -import type { StyleOptions } from './Style.js'; -import type { JsonReplacerFn } from './types.js'; +import type { JsonReplacerFn, StyleOptions, StyleTextFormatArray } from './types.js'; export type JsonConsoleOptions = { - style?: Partial; + style?: Partial | StyleTextFormatArray; space?: number | string; replacer?: JsonReplacerFn; }; @@ -28,10 +28,7 @@ export function createConsole(options: Partial = {}): Cons ? { ...stdOutJson, ...json[1], - style: { - ...stdOutJson?.style, - ...json[1]?.style, - }, + style: mergeStyleOptions(stdOutJson?.style, json[1]?.style), } : json; diff --git a/src/createSimpleStyleOptions.test.ts b/src/createSimpleStyleOptions.test.ts new file mode 100644 index 0000000..6f57f05 --- /dev/null +++ b/src/createSimpleStyleOptions.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; + +import { createSimpleStyleOptions } from './createSimpleStyleOptions.js'; + +import type { StyleOptions } from './types.js'; + +describe('createSimpleStyleOptions', () => { + it('creates style options with the given value for all style keys', () => { + expect(createSimpleStyleOptions(['red'])).toEqual({ + boolean: ['red'], + bracket: ['red'], + colon: ['red'], + comma: ['red'], + key: ['red'], + null: ['red'], + number: ['red'], + quoteKey: ['red'], + quoteString: ['red'], + string: ['red'], + } satisfies StyleOptions); + }); +}); diff --git a/src/createSimpleStyleOptions.ts b/src/createSimpleStyleOptions.ts new file mode 100644 index 0000000..fdf0232 --- /dev/null +++ b/src/createSimpleStyleOptions.ts @@ -0,0 +1,16 @@ +import { defaultStyleOptions } from './defaults.js'; +import { isStyleKey } from './isStyleKey.js'; + +import type { StyleOptions, StyleTextFormatArray } from './types.js'; + +export function createSimpleStyleOptions(value: StyleTextFormatArray): StyleOptions { + const options: StyleOptions = { ...defaultStyleOptions }; + + for (const key in options) { + if (isStyleKey(key)) { + options[key] = value; + } + } + + return options; +} diff --git a/src/defaults.ts b/src/defaults.ts new file mode 100644 index 0000000..b0640f1 --- /dev/null +++ b/src/defaults.ts @@ -0,0 +1,21 @@ +import type { StyleOptions } from './types.js'; + +export const defaultStyleOptions: Readonly = { + // content + string: ['green'], + number: ['yellowBright'], + boolean: ['blueBright'], + null: ['redBright'], + + // structural + bracket: ['white', 'blue', 'yellow', 'cyan', 'green', 'red'], + comma: ['white'], + colon: ['white'], + + // quotes + quoteKey: ['cyan'], + quoteString: ['green'], + + // keys + key: ['cyan'], +}; diff --git a/src/isStyleKey.test.ts b/src/isStyleKey.test.ts new file mode 100644 index 0000000..d73aa11 --- /dev/null +++ b/src/isStyleKey.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { isStyleKey } from './isStyleKey.js'; + +describe('isStyleKey()', () => { + it.each(['string', 'number', 'boolean', 'bracket', 'comma', 'colon', 'quoteKey', 'quoteString', 'key', 'null'])( + 'returns true for %j', + (input) => { + expect(isStyleKey(input)).toBe(true); + }, + ); + + it.each([null, undefined, 42, {}, [], 'not valid'])('returns false for %j', (input) => { + expect(isStyleKey(input)).toBe(false); + }); +}); diff --git a/src/isStyleKey.ts b/src/isStyleKey.ts new file mode 100644 index 0000000..9165c8f --- /dev/null +++ b/src/isStyleKey.ts @@ -0,0 +1,16 @@ +import type { StyleKey } from './types.js'; + +export const isStyleKey = (value: unknown): value is StyleKey => { + return ( + value === 'string' || + value === 'number' || + value === 'boolean' || + value === 'bracket' || + value === 'comma' || + value === 'colon' || + value === 'quoteKey' || + value === 'quoteString' || + value === 'key' || + value === 'null' + ); +}; diff --git a/src/mergeStyleOptions.test.ts b/src/mergeStyleOptions.test.ts new file mode 100644 index 0000000..9d3a532 --- /dev/null +++ b/src/mergeStyleOptions.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; + +import { defaultStyleOptions } from './defaults.js'; +import { mergeStyleOptions } from './mergeStyleOptions.js'; + +import type { StyleOptions } from './types.js'; + +describe('mergeStyleOptions()', () => { + it('merges two StyleOptions objects', () => { + expect( + mergeStyleOptions( + { + string: ['red'], + }, + { + string: ['blue'], + }, + ), + ).toEqual({ + ...defaultStyleOptions, + string: ['blue'], + } satisfies StyleOptions); + }); + + it('Builds StyleOptions when overrides is an array', () => { + expect( + mergeStyleOptions( + { + string: ['red'], + }, + ['blue'], + ), + ).toEqual({ + boolean: ['blue'], + bracket: ['blue'], + colon: ['blue'], + comma: ['blue'], + key: ['blue'], + null: ['blue'], + number: ['blue'], + quoteKey: ['blue'], + quoteString: ['blue'], + string: ['blue'], + } satisfies StyleOptions); + }); + + it('Builds StyleOptions from base if it is an array', () => { + expect(mergeStyleOptions(['red'], { string: ['blue'] })).toEqual({ + boolean: ['red'], + bracket: ['red'], + colon: ['red'], + comma: ['red'], + key: ['red'], + null: ['red'], + number: ['red'], + quoteKey: ['red'], + quoteString: ['red'], + string: ['blue'], + } satisfies StyleOptions); + }); +}); diff --git a/src/mergeStyleOptions.ts b/src/mergeStyleOptions.ts new file mode 100644 index 0000000..e341c49 --- /dev/null +++ b/src/mergeStyleOptions.ts @@ -0,0 +1,22 @@ +import { createSimpleStyleOptions } from './createSimpleStyleOptions.js'; +import { defaultStyleOptions } from './defaults.js'; + +import type { StyleOptions, StyleTextFormatArray } from './types.js'; + +export function mergeStyleOptions( + base?: Partial | StyleTextFormatArray, + override?: Partial | StyleTextFormatArray, +): StyleOptions { + if (Array.isArray(override)) { + return createSimpleStyleOptions(override); + } + + if (Array.isArray(base)) { + return { + ...createSimpleStyleOptions(base), + ...override, + }; + } + + return { ...defaultStyleOptions, ...base, ...override }; +} diff --git a/src/types.ts b/src/types.ts index ec9c89f..f04e4ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,11 @@ export type JsonReplacerFn = (this: unknown, key: string, value: unknown) => unk export type StyleTextFormat = Parameters[0]; +/** + * An array of `StyleTextFormat` with at least one item. + */ +export type StyleTextFormatArray = [item: StyleTextFormat, ...rest: StyleTextFormat[]]; + /** * @internal */ @@ -14,3 +19,17 @@ export type StyleTextBgColor = Extract; export type StyleTextColor = StyleTextBgColor extends `bg${infer Color}` ? Uncapitalize : never; export type StyleTextModifier = Exclude; + +export type StyleKey = + | 'string' + | 'number' + | 'boolean' + | 'bracket' + | 'comma' + | 'colon' + | 'quoteKey' + | 'quoteString' + | 'key' + | 'null'; + +export type StyleOptions = Record; diff --git a/tsconfig.example.json b/tsconfig.example.json new file mode 100644 index 0000000..5ca91c8 --- /dev/null +++ b/tsconfig.example.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "./example/", + "tsBuildInfoFile": "./cache/example.tsbuildinfo" + }, + "include": ["example/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json index 964faad..c50df5c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ }, "references": [ { "path": "./tsconfig.src.json" }, + { "path": "./tsconfig.example.json" }, { "path": "./tsconfig.test.json" }, { "path": "./tsconfig.configs.json" } ]