diff --git a/package-lock.json b/package-lock.json index c643a61..f29bce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -713,11 +713,15 @@ "@babel/types": "^7.3.0" } }, + "@types/browser-or-node": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/browser-or-node/-/browser-or-node-1.2.0.tgz", + "integrity": "sha512-hLn4jvpZ804yQDu71YW7qNQDm045XmODoEOZohkH4jWb23AaPodhVM5qztG+XM54Oqw8X1dA4A7z49iNFGbrxA==" + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/graceful-fs": { "version": "4.1.3", @@ -938,7 +942,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -1189,6 +1192,11 @@ "fill-range": "^7.0.1" } }, + "browser-or-node": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-1.3.0.tgz", + "integrity": "sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg==" + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -1264,10 +1272,9 @@ "dev": true }, "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1345,7 +1352,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -1353,8 +1359,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", @@ -2074,8 +2079,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "has-symbols": { "version": "1.0.1", @@ -4527,7 +4531,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, "requires": { "has-flag": "^4.0.0" } diff --git a/package.json b/package.json index fc553a7..ceea2ed 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,9 @@ }, "dependencies": { "@nascentdigital/errors": "^1.0.0", + "@types/browser-or-node": "^1.2.0", + "browser-or-node": "^1.3.0", + "chalk": "^4.1.0", "string.prototype.matchall": "^4.0.2" }, "devDependencies": { diff --git a/src/Log.ts b/src/Log.ts index a740148..0732740 100644 --- a/src/Log.ts +++ b/src/Log.ts @@ -12,7 +12,6 @@ export const LogLevels: ReadonlyArray = [ "silent" ]; export type LogMethod = Exclude; - export type LogParameter = string | number | boolean | ReadonlyArray | Readonly | undefined | null; export type LogFunction = (log: Log, method: LogMethod, message: LogParameter, ...args: ReadonlyArray) => void; export type LogContext = { diff --git a/src/Scribe.ts b/src/Scribe.ts index b08a079..65f62fc 100644 --- a/src/Scribe.ts +++ b/src/Scribe.ts @@ -3,13 +3,15 @@ import matchAll from "string.prototype.matchall"; import {ArgumentError, IllegalStateError} from "@nascentdigital/errors"; import { Log, - LogWriter, + LogContext, LogLevel, LogLevels, LogMethod, LogNamespace, LogNamespacePattern, - LogParameter, LogContext, LogTransform + LogParameter, + LogTransform, + LogWriter, } from "./Log"; import {ScribeLog} from "./ScribeLog"; import {ConsoleWriter} from "./writers"; @@ -50,10 +52,8 @@ export class Scribe { private static readonly _logs = new Map([[ROOT_NAMESPACE, Scribe._log]]); private static readonly _levelConfigs: Array = [ROOT_LOGLEVEL_CONFIG]; - public static get log() { return Scribe._log; } - public static reset() { // reset internals diff --git a/src/transforms/ColorTransformFactory.ts b/src/transforms/ColorTransformFactory.ts new file mode 100644 index 0000000..5a5d748 --- /dev/null +++ b/src/transforms/ColorTransformFactory.ts @@ -0,0 +1,297 @@ +// imports +import {NotImplementedError, RuntimeError} from "@nascentdigital/errors"; +import {isBrowser, isNode} from "browser-or-node" +import Chalk from'chalk' +import {LogContext, LogMethod, LogParameter, LogNamespace} from "../Log"; +import {Scribe} from '../Scribe'; + + +/** + * Thoughts by Sim: + * + * Interface for the Transform: + * 1. Options should provide some utility - you get something for setting them. "none" as an option seems to have no + * utility, since you might as well just not use the transform. + * 2. These options lack control - Why can't I set the colours, at least for "level" logging? + * + * Encapsulating via Modules: + * 1. You're violating the concept of "modules" - modules encapsulate functionality, not a namespace per-se, but they + * are larger than Classes (Java uses "packages", .NET uses "assemblies", NodeJS uses "modules"" + * - DLL/package/module(.js) -> Functions + Constants + Classes + etc. + * - Allows you to group common things, load them as a whole unit, hide shared / internal variables + functions + * 2. All code + types + state specific to a **module** should be contained by the module + * 3. Violations: + * - `LogColoringOption`, `LogColorRGB` in the "core" Log.ts + * - `Scribe.logColors` map in the global `Scribe` instance + * + * Naming: + * 1. Names seem really long, but clear + specific. You're 80% there - but the elegance comes to the distilling of + * the name. If you can shorten the name to something less than 12 characters - you're going to learn a lot about + * the function + your design. The name is usually too long because: + * a) The you're still too generic on or unclear of what it does + * b) You're trying to do too much with it + * c) You haven't contained it in something more specific (i.e. you have a global function that acutally should + * have prefixes of the name implied by the thing it belongs to - module, class, etc) + * + * + * TODO: + * 1. Fix this `LogColorRGB` type + * 2. Decouple browser vs non-browser handling... maybe later even pulling it out into a separate method + * - What's the best way / most reliable way to tell if you're in Node vs Browser (e.g. if (window), etc.) + * 3. Add some argument validation and custom exceptions (e.g. ArgumentOutOfRangeError if color is out of range) + * 3. Maybe change the `ColorTransform` function to be a factory class? + */ + +// error classes +class ArgumentOutOfRangeError extends RuntimeError {} +class UnsupportedEnvError extends RuntimeError {} +class UnsupportedFormatError extends RuntimeError {} + +// log color types +export type LogColorRGB = { + red: number; + green: number; + blue: number; +} +export type LogColorHSL = { + hue: number; + saturation: number; + lightness: number; +} +export type LogColor = LogColorRGB | LogColorHSL; + +// color transformation supported environments +enum SupportedEnvironments { + 'DESKTOP', + 'BROWSER' +} + +// utility functions +function isRGB(color: LogColor) : color is LogColorRGB { + return Object.prototype.hasOwnProperty.call(color, "red") +} + +function isHSL(color: LogColor) : color is LogColorHSL { + return Object.prototype.hasOwnProperty.call(color, "hue") +} + +function getEnvironment() { + if (isNode) { + return SupportedEnvironments.DESKTOP + } + else if (isBrowser) { + return SupportedEnvironments.BROWSER + } + else { + return undefined + } +} + +class ColorValidator { + constructor( + private _color: LogColor + ) {} + + _validateRGB(rgb: LogColorRGB) { + // red should be between 0 ~ 255 + if (rgb.red < 0 || rgb.red > 255) throw new ArgumentOutOfRangeError('value of red should be between 0 to 255') + // green should be between 0 ~ 255 + if (rgb.green < 0 || rgb.green > 255) throw new ArgumentOutOfRangeError('value of green should be between 0 to 255') + // blue should be between 0 ~ 255 + if (rgb.blue < 0 || rgb.blue > 255) throw new ArgumentOutOfRangeError('value of blue should be between 0 to 255') + } + + _validateHSL(hsl: LogColorHSL) { + // hue should be between 0 ~ 360 + if (hsl.hue < 0 || hsl.hue > 360) throw new ArgumentOutOfRangeError('value of hue should be between 0 to 360') + // saturation should be between 0 ~ 1 + if (hsl.saturation < 0 || hsl.saturation > 1) throw new ArgumentOutOfRangeError('value of saturation should be between 0 to 1') + // lightness should be between 0 ~ 1 + if (hsl.lightness < 0 || hsl.lightness > 1) throw new ArgumentOutOfRangeError('value of lightness should be between 0 to 1') + } + + /** + * method that validates if the given LogColor is valid + * @throws {ArgumentOutOfRangeError} if any of the color values are out of range + * @throws {} + */ + validate() { + if (isRGB(this._color)) { + this._validateRGB(this._color) + } + else if (isHSL(this._color)) { + this._validateHSL(this._color) + } + else { + throw new UnsupportedFormatError('The color format should be either RGB or HSL.') + } + } +} + +export abstract class ColoringStrategy { + abstract getColor(context: LogContext): LogColor; +} + +/** + * This strategy can be used to apply color based on the log's namespace. + * a random color is generated for each namespace + */ +export class NamespaceColoringStrategy extends ColoringStrategy { + + private readonly _namespaceColors: Map = new Map() + + public getColor(context: LogContext): LogColor { + + // get namespace from the context + const namespace = context.log.namespace + + // find the log color for this namespace + if (this._namespaceColors.has(namespace)) { + + // return the existing color + return this._namespaceColors.get(namespace) as LogColor + } + + // create a new color for this namespace since it doesn't already exist + const logColor: LogColorRGB = { + red: 255 * Math.random(), + green: 255 * Math.random(), + blue: 255 * Math.random(), + } + + // set the new color as the color for this namespace + this._namespaceColors.set(namespace, logColor) + + // return the new color + return logColor + } +} + +/** + * This strategy can be used to apply color based on the log level + */ +export class LevelColoringStrategy extends ColoringStrategy { + /** + * @param _levelColors a mapping from log method to its RGB color + */ + constructor(private _levelColors: Record) { + super(); + } + + public getColor(context: LogContext): LogColor { + return this._levelColors[context.method] + } +} + +export class ColorTransformFactory { + /** + * @param strategy strategy for coloring the log message + */ + static create(strategy: ColoringStrategy) { + return function (context: LogContext) { + + // get color + const color = strategy.getColor(context) + + // validate color + const colorValidator = new ColorValidator(color) + colorValidator.validate() + + // TODO: abstract the transforming from color -> message + // apply color to message + let message = context.message; + + // get the current environment + const env = getEnvironment() + + // check if the code is running on desktop + if (env === SupportedEnvironments.DESKTOP) { + // convert the color to a Chalk + const chalk = isRGB(color) + ? Chalk.rgb(color.red, color.green, color.blue) + : Chalk.hsl(color.hue, color.saturation*100.0, color.lightness*100.0) + + // convert message + message = chalk(message) + } + + // apply for browser + else if (env == SupportedEnvironments.BROWSER) { + throw new NotImplementedError('Browser support comming soon!') + } + + // any other environments are unsupported + else { + throw new UnsupportedEnvError('Color transform only supports browser and node environment'); + } + + // return transfomed context + return Object.assign({}, context, {message}); + } + } +} + +function test() { + const globalLog = Scribe.log + + Scribe.transform = ColorTransformFactory.create(new LevelColoringStrategy({ + 'trace': { + 'red': 0, + 'green': 0, + 'blue': 0 + } as LogColorRGB, + 'debug': { + 'hue': 30, + 'saturation': 0.8, + 'lightness': 0.5 + } as LogColorHSL, + 'info': { + 'red': 100, + 'green': 23, + 'blue': 160 + } as LogColorRGB, + 'warn': { + 'red': 230, + 'green': 22, + 'blue': 190 + } as LogColorRGB, + 'error': { + 'red': 0, + 'green': 50, + 'blue': 200 + } as LogColorRGB, + })) + + globalLog.trace('trace log') + globalLog.debug('debug log') + globalLog.info('info log') + globalLog.warn('warn log') + globalLog.error('error log') + + const logForModuleA = Scribe.getLog("moduleA") + const logForModuleAMethodFoo = Scribe.getLog("moduleA:foo") + const logForModuleB = Scribe.getLog("moduleB") + const logForModuleC = Scribe.getLog("moduleC") + const logForModuleD = Scribe.getLog("moduleD") + const logForModuleE = Scribe.getLog("moduleE") + const logForModuleF = Scribe.getLog("moduleF") + const logForModuleG = Scribe.getLog("moduleG") + const logForModuleH = Scribe.getLog("moduleH") + + Scribe.transform = ColorTransformFactory.create(new NamespaceColoringStrategy()) + + logForModuleA.debug("debug message from moduleA") + logForModuleA.debug("debug message from moduleA") + logForModuleAMethodFoo.debug("debug message from moduleA:foo") + logForModuleAMethodFoo.debug("debug message from moduleA:foo") + logForModuleB.debug("debug message from moduleB") + logForModuleB.debug("debug message from moduleB") + logForModuleC.debug("debug message from moduleC") + logForModuleD.debug("debug message from moduleD") + logForModuleE.debug("debug message from moduleE") + logForModuleF.debug("debug message from moduleF") + logForModuleG.debug("debug message from moduleG") + logForModuleH.debug("debug message from moduleH") + +} + diff --git a/src/transforms/index.ts b/src/transforms/index.ts index bd0eb5e..b0db553 100644 --- a/src/transforms/index.ts +++ b/src/transforms/index.ts @@ -2,3 +2,5 @@ export * from "./IdentityTransform"; export * from "./PrefixTransform"; export * from "./TransformBuilder"; +export * from "./ColorTransformFactory"; +