├── .nvmrc ├── .eslintignore ├── docs ├── PrefixFactory.md ├── MethodFactory.md └── LogLevel.md ├── .prettierrc ├── .npmrc ├── tsconfig.json ├── .commitlintrc ├── test ├── tsconfig.json ├── .eslintrc ├── methods.ts ├── api.ts ├── prefix-factory.ts ├── factory.ts └── levels.ts ├── assets ├── loglevelnext-icon.ai └── loglevelnext-icon.svg ├── .gitignore ├── tsconfig.eslint.json ├── .eslintrc.js ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── workflows │ └── validate.yml ├── tsconfig.base.json ├── CONTRIBUTING.md ├── src ├── index.ts ├── PrefixFactory.ts ├── LogLevel.ts └── MethodFactory.ts ├── package.json ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /docs/PrefixFactory.md: -------------------------------------------------------------------------------- 1 | # PrefixFactory 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "eslint-config-shellscape/configs/prettier.json" 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # pnpm options 2 | enable-pre-post-scripts = true 3 | shamefully-hoist = true 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "include": ["."] 4 | } 5 | -------------------------------------------------------------------------------- /assets/loglevelnext-icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/loglevelnext/HEAD/assets/loglevelnext-icon.ai -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintcache 3 | pnpm-debug.log 4 | coverage 5 | node_modules 6 | dist 7 | coverage* 8 | .nyc* 9 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src", "test", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": "off", 4 | "no-console": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'shellscape/typescript', 3 | parserOptions: { 4 | project: ['./tsconfig.eslint.json'], 5 | tsconfigRootDir: __dirname 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Which issue #** if any, does this resolve? 2 | 3 | 4 | 5 | Please check one: 6 | 7 | - [ ] New tests created for this change 8 | - [ ] Tests updated for this change 9 | 10 | This PR: 11 | 12 | - [ ] Adds new API 13 | - [ ] Extends existing API, backwards-compatible 14 | - [ ] Introduces a breaking change 15 | - [ ] Fixes a bug 16 | 17 | --- 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing in `loglevelnext` 2 | 3 | We 💛 contributions! The rules for contributing to this project are few: 4 | 5 | 1. Don't be a jerk 6 | 1. Ask nicely 7 | 1. Search issues before opening a new one 8 | 1. Lint and run tests locally before submitting a PR 9 | 1. Adhere to the code style the project has chosen 10 | 11 | ## Dependency Management 12 | 13 | This project uses [pnpm](https://pnpm.io/) for managing dependencies. Please don't commit `package-lock.json` or `yarn.lock` files in contributions. -------------------------------------------------------------------------------- /test/methods.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import log from '../src'; 4 | import { LogLevel } from '../src/LogLevel'; 5 | 6 | const levels = Object.keys(log.levels) 7 | .map((key) => key.toLowerCase()) 8 | .filter((key) => key !== 'silent'); 9 | 10 | // console.debug is aliased to console.log 11 | levels.push('log'); 12 | 13 | test('exists', (t) => { 14 | t.truthy(log); 15 | t.true(log instanceof LogLevel); 16 | }); 17 | 18 | test('has logging methods', (t) => { 19 | for (const level of levels) { 20 | t.is(typeof log[level], 'function'); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "lib": ["ES2018", "ES2020", "esnext.asynciterable", "dom"], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "noEmitOnError": false, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "outDir": "dist", 15 | "pretty": true, 16 | "removeComments": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "sourceMap": true, 20 | "strict": true, 21 | "target": "es2020" 22 | }, 23 | "exclude": ["dist", "node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | - Node Version: 8 | - NPM Version: 9 | - loglevelnext Version: 10 | 11 | If you have a large amount of code to share which demonstrates the problem you're experiencing, please provide a link to your 12 | repository rather than pasting code. Otherwise, please paste relevant short snippets below. 13 | 14 | ### LESS 15 | 16 | ```less 17 | // less that demonstrates the issue 18 | ``` 19 | 20 | ### JavaScript 21 | 22 | ```js 23 | // js that reproduces the issue 24 | ``` 25 | 26 | ### Errors 27 | 28 | ``` 29 | // actual error output, if error was thrown 30 | ``` 31 | 32 | ### Expected Behavior 33 | 34 | ### Actual Behavior 35 | 36 | ### How can we reproduce the behavior? 37 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - edited 7 | - opened 8 | - synchronize 9 | push: 10 | branches: 11 | - "*" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node: [20] 20 | 21 | name: Node ${{ matrix.node }} 22 | 23 | steps: 24 | - name: Checkout Commit 25 | uses: actions/checkout@v1 26 | 27 | - name: Setup Node 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node }} 31 | 32 | - name: Install pnpm 33 | run: npm install pnpm -g 34 | 35 | - name: Sanity Check 36 | run: | 37 | echo branch `git branch --show-current`; 38 | echo node `node -v`; 39 | echo pnpm `pnpm -v` 40 | 41 | - name: pnpm install 42 | run: pnpm install 43 | 44 | - name: Build 45 | run: pnpm build 46 | 47 | - name: Lint 48 | run: pnpm lint 49 | 50 | - name: Test and Coverage 51 | run: pnpm ci:coverage 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome! 2 | :heart: Thanks for your interest and time in contributing to this project. 3 | 4 | ## What We Use 5 | 6 | - Building: [Webpack](https://webpack.js.org) 7 | - Linting: [ESLint](http://eslint.org/) 8 | - NPM: [NPM as a Build Tool](https://css-tricks.com/using-npm-build-tool/) 9 | - Testing: [Mocha](https://mochajs.org) 10 | 11 | ## Pull Requests 12 | 13 | Please lint your changes before submitting a Pull Request. You can lint your 14 | changes by running: 15 | 16 | ```console 17 | $ npm run lint 18 | ``` 19 | 20 | Please avoid committing `package-lock.json` files. 21 | 22 | Please don't change variable or parameter names to match your 23 | personal preferences, unless the change is part of a refactor 24 | or significant modification of the codebase (which is very rare). 25 | 26 | Please remember to thoroughly explain your Pull Request if it 27 | doesn't have an associated issue. If you're changing code 28 | significantly, please remember to add inline or block comments 29 | in the code as appropriate. 30 | 31 | ## Thanks 32 | 33 | For your interest, time, understanding, and for following this simple guide. 34 | -------------------------------------------------------------------------------- /docs/MethodFactory.md: -------------------------------------------------------------------------------- 1 | # MethodFactory 2 | 3 | The `MethodFactory`, and subsequent factories that inherit from it, are 4 | responsible for wrapping `console` methods to provide logging functionality. 5 | 6 | ## Methods 7 | 8 | ### `bindMethod(obj, methodName)` 9 | 10 | Binds a corresponding `console` method to the object specified. 11 | 12 | Returns: `Function` 13 | 14 | ### Parameters 15 | 16 | #### `obj` 17 | 18 | Type: `Object` 19 | 20 | #### `methodName` 21 | 22 | Type: `String` 23 | 24 | ### `distillLevel(level)` 25 | 26 | Processes a valid level value and returns the numerical value. 27 | 28 | Returns: `Number` 29 | 30 | ### Parameters 31 | 32 | #### `level` 33 | 34 | Type: `Any` 35 | 36 | The level to process. 37 | 38 | ### `levelValid(level)` 39 | 40 | Determines if a given log level corresponds to a name or value in `levels`. 41 | 42 | Returns: `Boolean` 43 | 44 | ### Parameters 45 | 46 | #### `level` 47 | 48 | Type: `Any` 49 | 50 | The level to check. 51 | 52 | ### `make(methodName)` 53 | 54 | Contains logic for binding the specified method and adding it to the logger. 55 | 56 | ### Parameters 57 | 58 | #### `methodName` 59 | 60 | Type: `String` 61 | 62 | The method to create. 63 | 64 | ## `replaceMethods(logLevel)` 65 | 66 | The entry point for `LogLevel` instances to request binding and wrapping of log 67 | methods. 68 | 69 | ### Parameters 70 | 71 | #### `logLevel` 72 | 73 | Type: `String|Number` 74 | 75 | Specifies the minimum level that will produce log output. The value must be a 76 | `String` or `Number.` 77 | 78 | ### `levels` 79 | 80 | Type: `Object` 81 | 82 | Gets an object which represents the valid level name-value pairs for this log 83 | instance. The return value will match the same property in 84 | [`LogLevel`](LogLevel.md). 85 | 86 | ### `logger` 87 | 88 | Type: `LogLevel` 89 | 90 | Gets or sets the logger that the factory will operate on. 91 | 92 | ### `methods` 93 | 94 | Type: `Array [ String ]` 95 | 96 | Gets an `Array`of `String` containing the list of methods that the Factory will 97 | wrap for the logger. This list is distilled from the `levels` property. 98 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | 12 | import { LogLevel, LogLevelOptions } from './LogLevel'; 13 | import { MethodFactory } from './MethodFactory'; 14 | import { PrefixFactory } from './PrefixFactory'; 15 | 16 | export * from './LogLevel'; 17 | export * from './MethodFactory'; 18 | export * from './PrefixFactory'; 19 | 20 | const factories = Symbol('log-factories'); 21 | 22 | class DefaultLogger extends LogLevel { 23 | private cache: Record; 24 | 25 | constructor() { 26 | super({ name: 'default' }); 27 | 28 | this.cache = { default: this as LogLevel }; 29 | // TS can't handle symbols as index types 30 | this[factories as any] = { MethodFactory, PrefixFactory }; 31 | } 32 | 33 | get factories() { 34 | return this[factories as any]; 35 | } 36 | 37 | get loggers() { 38 | return this.cache; 39 | } 40 | 41 | create(opts: LogLevelOptions | string) { 42 | let options: LogLevelOptions; 43 | 44 | if (typeof opts === 'string') { 45 | options = { name: opts }; 46 | } else { 47 | options = Object.assign({}, opts); 48 | } 49 | 50 | if (!options.id) { 51 | options.id = options.name!.toString(); 52 | } 53 | 54 | const { name, id } = options; 55 | const defaults = { level: this.level }; 56 | 57 | if (typeof name !== 'string' || !name || !name.length) { 58 | throw new TypeError('You must supply a name when creating a logger.'); 59 | } 60 | 61 | let logger = this.cache[id]; 62 | if (!logger) { 63 | logger = new LogLevel(Object.assign({}, defaults, options)); 64 | this.cache[id] = logger; 65 | } 66 | return logger; 67 | } 68 | } 69 | 70 | // eslint-disable-next-line import/no-default-export 71 | export default new DefaultLogger(); 72 | -------------------------------------------------------------------------------- /test/api.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import log from '../src'; 4 | import { LogLevel } from '../src/LogLevel'; 5 | import { MethodFactory } from '../src/MethodFactory'; 6 | import { PrefixFactory } from '../src/PrefixFactory'; 7 | 8 | test('has extended methods', (t) => { 9 | t.is(typeof log.loggers, 'object'); 10 | t.is(typeof log.create, 'function'); 11 | }); 12 | 13 | test('returns factories', (t) => { 14 | t.truthy(log.factories); 15 | t.deepEqual(log.factories.MethodFactory, MethodFactory); 16 | t.deepEqual(log.factories.PrefixFactory, PrefixFactory); 17 | }); 18 | 19 | test('returns loggers cache', (t) => { 20 | t.truthy(log.loggers); 21 | t.deepEqual(log.loggers.default, log as any); 22 | }); 23 | 24 | test('gets a new logger', (t) => { 25 | const child = log.create('child'); 26 | t.truthy(child); 27 | t.notDeepEqual(child, log as any); 28 | t.truthy(child instanceof LogLevel); 29 | t.is(typeof child.create, 'undefined'); 30 | t.truthy(log.loggers.child); 31 | }); 32 | 33 | test('create() returns the same instance', (t) => { 34 | const child1 = log.create('new'); 35 | const child2 = log.create('new'); 36 | 37 | t.deepEqual(child1, child2); 38 | }); 39 | 40 | test('create() returns different instances', (t) => { 41 | const child1 = log.create({ id: (+new Date()).toString(), name: 'newer' }); 42 | const child2 = log.create({ id: (+new Date() + 1).toString(), name: 'newer' }); 43 | 44 | t.notDeepEqual(child1, child2); 45 | }); 46 | 47 | test('create() throws if called with no name / empty name', (t) => { 48 | t.throws(() => { 49 | (log as any).create(); 50 | }); 51 | t.throws(() => { 52 | log.create(''); 53 | }); 54 | t.throws(() => { 55 | (log as any).create(true); 56 | }); 57 | }); 58 | 59 | test('child logger created with the same level', (t) => { 60 | log.level = 'error'; 61 | const child = log.create('child2'); 62 | t.is(child.level, log.level); 63 | }); 64 | 65 | test("other loggers do not change when the default logger's level is changed", (t) => { 66 | const child = log.create('child'); 67 | 68 | child.level = 'warn'; 69 | log.level = 'silent'; 70 | 71 | t.is(log.level, 5); 72 | t.is(child.level, 3); 73 | }); 74 | -------------------------------------------------------------------------------- /src/PrefixFactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | 12 | import { LogLevel } from './LogLevel'; 13 | import { MethodFactory } from './MethodFactory'; 14 | 15 | export interface PrefixTemplateOptions { 16 | level: string; 17 | logger: LogLevel; 18 | } 19 | 20 | export type PrefixTemplateFn = (options: PrefixTemplateOptions) => string; 21 | 22 | export interface PrefixFactoryOptions { 23 | [key: string]: PrefixTemplateFn | string | undefined; 24 | level?: PrefixTemplateFn; 25 | name?: PrefixTemplateFn; 26 | template?: string; 27 | time?: PrefixTemplateFn; 28 | } 29 | 30 | const defaults: PrefixFactoryOptions = { 31 | level: (opts) => `[${opts.level}]`, 32 | name: (opts) => opts.logger.name, 33 | template: '{{time}} {{level}} ', 34 | time: () => new Date().toTimeString().split(' ')[0] 35 | }; 36 | 37 | export class PrefixFactory extends MethodFactory { 38 | private options: PrefixFactoryOptions; 39 | 40 | constructor(logger?: LogLevel, options?: PrefixFactoryOptions) { 41 | super(logger); 42 | this.options = Object.assign({}, defaults, options); 43 | } 44 | 45 | interpolate(level: string) { 46 | return this.options.template!.replace(/{{([^{}]*)}}/g, (stache: string, prop: string) => { 47 | const fn = this.options[prop]; 48 | 49 | if (typeof fn === 'function') { 50 | return fn({ level, logger: this.logger }); 51 | } 52 | 53 | return stache; 54 | }); 55 | } 56 | 57 | make(methodName: string) { 58 | const og = super.make(methodName); 59 | 60 | return (...args: any[]) => { 61 | const output = this.interpolate(methodName); 62 | const [first] = args; 63 | 64 | if (typeof first === 'string') { 65 | // eslint-disable-next-line no-param-reassign 66 | args[0] = output + first; 67 | } else { 68 | args.unshift(output); 69 | } 70 | 71 | og(...args); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/prefix-factory.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | 4 | import { LogLevel } from '../src/LogLevel'; 5 | import { Factory } from '../src/MethodFactory'; 6 | import { PrefixFactory } from '../src/PrefixFactory'; 7 | 8 | const sandbox = sinon.createSandbox(); 9 | 10 | let log: LogLevel; 11 | let factory: Factory; 12 | let spy: sinon.SinonSpy; 13 | 14 | test.before(() => { 15 | spy = sandbox.spy(console, 'info'); 16 | log = new LogLevel({ 17 | level: 'trace', 18 | name: 'test', 19 | prefix: {} 20 | }); 21 | factory = log.factory; // eslint-disable-line 22 | }); 23 | 24 | test.afterEach(() => { 25 | spy.resetHistory(); 26 | }); 27 | 28 | test.after(() => { 29 | sandbox.restore(); 30 | }); 31 | 32 | test.serial('created a PrefixFactory', (t) => { 33 | t.truthy(factory instanceof PrefixFactory); 34 | }); 35 | 36 | test.serial('gets the name from the base logger', (t) => { 37 | t.is(factory.options.name({ logger: log }), 'test'); 38 | }); 39 | 40 | test.serial('prefixes output', (t) => { 41 | log.info('foo'); 42 | 43 | const [first] = spy.firstCall.args; 44 | 45 | t.is(spy.callCount, 1); 46 | t.truthy(/\d{2}:\d{2}:\d{2}\s\[info\]\sfoo/.test(first)); 47 | }); 48 | 49 | test.serial('prefixes output with custom options', (t) => { 50 | const options = { 51 | level: (opts: any) => `[${opts.level.substring(1)}]`, 52 | name: (opts: any) => opts.logger.name.toUpperCase(), 53 | template: '{{time}} {{level}} ({{name}}) {{nope}}-', 54 | time: () => `[${new Date().toTimeString().split(' ')[0].split(':')[0]}]` 55 | }; 56 | const customPrefix = new PrefixFactory(log, options); 57 | 58 | log.factory = customPrefix; 59 | log.info('foo'); 60 | 61 | const [first] = spy.firstCall.args; 62 | const terped = customPrefix.interpolate('info'); 63 | const rOutput = /\[\d{2}\]\s\[nfo\]\s\(TEST\)\s\{\{nope\}\}-/; 64 | 65 | t.truthy(rOutput.test(terped)); 66 | t.is(spy.callCount, 1); 67 | t.truthy(/\[\d{2}\]\s\[nfo\]\s\(TEST\)\s\{\{nope\}\}-foo/.test(first)); 68 | 69 | // test the first argument when passing a non-string 70 | log.info({}); 71 | 72 | const [last] = spy.lastCall.args; 73 | t.truthy(rOutput.test(last)); 74 | }); 75 | 76 | test.serial('supports different prefixes per logger', (t) => { 77 | const log2 = new LogLevel({ 78 | level: 'trace', 79 | name: 'test', 80 | prefix: { template: 'baz ' } 81 | }); 82 | 83 | log.info('foo'); 84 | log2.info('foo'); 85 | 86 | const [first] = spy.firstCall.args; 87 | const [last] = spy.lastCall.args; 88 | 89 | t.is(spy.callCount, 2); 90 | t.not(first, last); 91 | }); 92 | -------------------------------------------------------------------------------- /test/factory.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | 4 | import { LogLevel } from '../src/LogLevel'; 5 | import { MethodFactory } from '../src/MethodFactory'; 6 | 7 | /* eslint-disable sort-keys */ 8 | const levels = { 9 | TRACE: 0, 10 | DEBUG: 1, 11 | INFO: 2, 12 | WARN: 3, 13 | ERROR: 4, 14 | SILENT: 5 15 | }; 16 | /* eslint-enable sort-keys */ 17 | 18 | const methods = ['trace', 'debug', 'info', 'warn', 'error']; 19 | 20 | const factory = new MethodFactory(); 21 | const sandbox = sinon.createSandbox(); 22 | let log: LogLevel; 23 | let spy: sinon.SinonSpy; 24 | 25 | test.before(() => { 26 | spy = sandbox.spy(factory, 'make'); 27 | }); 28 | 29 | test.after(() => { 30 | sandbox.restore(); 31 | }); 32 | 33 | test.serial('gets levels', (t) => { 34 | t.deepEqual(factory.levels, levels); 35 | }); 36 | 37 | test.serial('gets methods', (t) => { 38 | t.deepEqual(factory.methods, methods); 39 | }); 40 | 41 | test.serial('throws on replaceMethods() with invalid level', (t) => { 42 | t.throws(() => { 43 | factory.replaceMethods(null as any); 44 | }); 45 | }); 46 | 47 | test.serial('throws on replaceMethods() with no logger defined', (t) => { 48 | t.throws(() => { 49 | factory.replaceMethods(0); 50 | }); 51 | }); 52 | 53 | test.serial('equals the log factory', (t) => { 54 | log = new LogLevel({ 55 | factory, 56 | level: 'trace', 57 | name: 'test' 58 | }); 59 | t.deepEqual(log.factory, factory); 60 | }); 61 | 62 | test.serial('calls make() for each method', (t) => { 63 | t.is(spy.callCount, factory.methods.length); 64 | 65 | const calls = spy.getCalls(); 66 | 67 | for (const [index, method] of factory.methods.entries()) { 68 | t.is(calls[index].args[0], method); 69 | } 70 | 71 | spy.resetHistory(); 72 | }); 73 | 74 | test.serial('calls make() for appropriate levels', (t) => { 75 | log.level = 'info'; 76 | 77 | t.is(spy.callCount, factory.levels.INFO + 1); 78 | 79 | const calls = spy.getCalls(); 80 | const checkMethods = factory.methods.slice(factory.levels.INFO); 81 | 82 | for (const [index, method] of checkMethods.entries()) { 83 | t.is(calls[index].args[0], method); 84 | } 85 | }); 86 | 87 | test.serial('sets the factory and calls make()', (t) => { 88 | const newFactory = new MethodFactory(log); 89 | 90 | const newSpy = sandbox.spy(newFactory, 'make'); 91 | log.factory = newFactory; 92 | 93 | t.deepEqual(log.factory, newFactory); 94 | t.is(newSpy.callCount, factory.levels.INFO + 1); 95 | 96 | const calls = spy.getCalls(); 97 | const checkMethods = factory.methods.slice(factory.levels.INFO); 98 | 99 | for (const [index, method] of checkMethods.entries()) { 100 | t.is(calls[index].args[0], method); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /test/levels.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import sinon from 'sinon'; 3 | import test from 'ava'; 4 | 5 | import log from '../src'; 6 | 7 | const sandbox = sinon.createSandbox(); 8 | const spyMethods = Object.keys(log.levels) 9 | .map((key) => key.toLowerCase()) 10 | .filter((key) => key !== 'silent'); 11 | 12 | interface ConsoleIndex { 13 | [key: string]: Function; 14 | } 15 | 16 | const con = console as unknown as ConsoleIndex; 17 | 18 | test.before(() => { 19 | for (const method of spyMethods) { 20 | if (con[method]) { 21 | // curiously, node 6 doesn't have console.debug 22 | sandbox.spy(con, method as any); 23 | } 24 | } 25 | sandbox.spy(con, 'log'); 26 | }); 27 | 28 | test.afterEach(() => { 29 | for (const method of spyMethods) { 30 | if (con[method]) { 31 | (con[method] as sinon.SinonSpy).resetHistory(); 32 | } 33 | } 34 | (con.log as sinon.SinonSpy).resetHistory(); 35 | }); 36 | 37 | test.after(() => { 38 | sandbox.restore(); 39 | }); 40 | 41 | test.serial('can set all levels', (t) => { 42 | log.level = log.levels.TRACE; 43 | log.level = log.levels.DEBUG; 44 | log.level = log.levels.INFO; 45 | log.level = log.levels.WARN; 46 | log.level = log.levels.ERROR; 47 | log.level = log.levels.SILENT; 48 | 49 | t.pass(); 50 | }); 51 | 52 | for (const name of Object.keys(log.levels)) { 53 | const levelName: Uppercase = name as any; 54 | const { [levelName]: level } = log.levels; 55 | 56 | test.serial(`sets level ${name}`, (t) => { 57 | log.level = name; 58 | t.is(log.level, level); 59 | }); 60 | 61 | test.serial(`logs only levels >= ${name}`, (t) => { 62 | for (const method of spyMethods) { 63 | let expected = 1; 64 | 65 | // NOTE: the [object Object] + stack output to console is part of the 'trace' test. fret not. 66 | log[method](chalk.black(`test ${method}`)); 67 | 68 | if (level > log.levels[method.toUpperCase() as Uppercase]) { 69 | expected = 0; 70 | } 71 | 72 | // This was true for earlier node versions 73 | // if (method === 'error' && name === 'TRACE' && expected === 1) { 74 | // expected = 2; 75 | // } 76 | 77 | t.is((con[method] as sinon.SinonSpy).callCount, expected); 78 | } 79 | }); 80 | } 81 | 82 | test.serial('disable() sets SILENT', (t) => { 83 | log.disable(); 84 | t.is(log.level, log.levels.SILENT); 85 | }); 86 | 87 | test.serial('enable() sets TRACE', (t) => { 88 | log.enable(); 89 | t.is(log.level, log.levels.TRACE); 90 | }); 91 | 92 | test.serial('throws on invalid levels', (t) => { 93 | t.throws(() => { 94 | (log as any).level = null; 95 | }); 96 | t.throws(() => { 97 | // eslint-disable-next-line no-undefined 98 | (log as any).level = undefined; 99 | }); 100 | t.throws(() => { 101 | log.level = -1; 102 | }); 103 | t.throws(() => { 104 | log.level = 'foo'; 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loglevelnext", 3 | "version": "6.0.0", 4 | "description": "A modern logging library for Node.js and modern browsers that provides log level mapping to the console", 5 | "license": "MPL-2.0", 6 | "repository": "shellscape/loglevelnext", 7 | "author": "Andrew Powell ", 8 | "homepage": "https://github.com/shellscape/loglevelnext", 9 | "main": "./dist/index.js", 10 | "module": "./dist/index.mjs", 11 | "exports": { 12 | ".": { 13 | "import": { 14 | "types": "./dist/index.d.ts", 15 | "default": "./dist/index.mjs" 16 | }, 17 | "require": { 18 | "types": "./dist/index.d.ts", 19 | "default": "./dist/index.js" 20 | } 21 | } 22 | }, 23 | "engines": { 24 | "node": ">= 18" 25 | }, 26 | "scripts": { 27 | "build": "tsup src/index.ts --format esm,cjs --dts", 28 | "ci:coverage": "nyc pnpm test && nyc report --reporter=text-lcov > coverage.lcov", 29 | "commitlint": "commitlint", 30 | "commitmsg": "commitlint -e $GIT_PARAMS", 31 | "lint": "pnpm lint:docs && pnpm lint:json && pnpm lint:package && pnpm lint:js", 32 | "lint-staged": "lint-staged", 33 | "lint:docs": "prettier --write .github/**/*.md **/README.md", 34 | "lint:js": "eslint --fix --cache src test", 35 | "lint:json": "prettier --write tsconfig.*.json", 36 | "lint:package": "prettier --write **/package.json --plugin=prettier-plugin-package", 37 | "prebuild": "tsc --project tsconfig.json", 38 | "prepublishOnly": "pnpm build", 39 | "security": "pnpm audit --audit-level high", 40 | "test": "FORCE_COLOR=3 ava" 41 | }, 42 | "files": [ 43 | "dist", 44 | "README.md" 45 | ], 46 | "keywords": [ 47 | "browser", 48 | "console", 49 | "debug", 50 | "error", 51 | "level", 52 | "levels", 53 | "log", 54 | "logger", 55 | "logging", 56 | "loglevel", 57 | "persist", 58 | "persistent", 59 | "plugins", 60 | "prefix", 61 | "trace", 62 | "warn" 63 | ], 64 | "devDependencies": { 65 | "@commitlint/cli": "17.8.0", 66 | "@commitlint/config-conventional": "17.8.0", 67 | "@types/node": "^20.8.6", 68 | "@types/sinon": "^10.0.2", 69 | "ava": "^5.3.1", 70 | "chalk": "^4.0.0", 71 | "eslint-config-shellscape": "^6.0.1", 72 | "husky": "^8.0.3", 73 | "lint-staged": "15.0.1", 74 | "nyc": "^15.0.1", 75 | "pre-commit": "^1.2.2", 76 | "sinon": "11.1.1", 77 | "ts-node": "^10.0.0", 78 | "tsup": "^7.2.0", 79 | "typescript": "^5.2.2" 80 | }, 81 | "types": "./dist/index.d.ts", 82 | "ava": { 83 | "extensions": [ 84 | "ts" 85 | ], 86 | "require": [ 87 | "ts-node/register" 88 | ], 89 | "files": [ 90 | "test/*.ts" 91 | ] 92 | }, 93 | "husky": { 94 | "hooks": { 95 | "pre-commit": "lint-staged" 96 | } 97 | }, 98 | "lint-staged": { 99 | "*.{ts,js}": [ 100 | "eslint --fix" 101 | ], 102 | "*.{json,md,yml,yaml}": [ 103 | "prettier --write" 104 | ], 105 | "*package.json": [ 106 | "prettier --write --plugin=prettier-plugin-package" 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/LogLevel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | 12 | import { defaultLevels, Factory, MethodFactory, type FactoryLevels } from './MethodFactory'; 13 | import { PrefixFactory, PrefixFactoryOptions } from './PrefixFactory'; 14 | 15 | type SomeLevel = number | string; 16 | 17 | export interface LogLevelIndex { 18 | [key: string]: any; 19 | } 20 | 21 | export interface LogLevelOptions { 22 | factory?: Factory; 23 | id?: string; 24 | level?: number | string; 25 | name?: string; 26 | prefix?: PrefixFactoryOptions; 27 | } 28 | 29 | const defaults: LogLevelOptions = { 30 | factory: void 0, 31 | level: 'warn', 32 | name: (+new Date()).toString(), 33 | prefix: void 0 34 | }; 35 | 36 | export class LogLevel 37 | implements LogLevelIndex 38 | { 39 | [key: string]: any; 40 | public name: string; 41 | 42 | public type: string; 43 | private currentLevel!: TLevels[keyof TLevels]; 44 | private methodFactory: Factory | undefined; 45 | private options: LogLevelOptions; 46 | 47 | constructor(options: LogLevelOptions) { 48 | // implement for some _very_ loose type checking. avoids getting into a 49 | // circular require between MethodFactory and LogLevel 50 | this.type = 'LogLevel'; 51 | this.options = Object.assign({}, defaults, options); 52 | this.methodFactory = options.factory; 53 | 54 | if (!(this as any).methodFactory) { 55 | const factory = options.prefix 56 | ? new PrefixFactory(this as any, options.prefix) 57 | : new MethodFactory(this as any); 58 | this.methodFactory = factory as any; 59 | } 60 | 61 | if (!this.methodFactory!.logger) { 62 | this.methodFactory!.logger = this as any; 63 | } 64 | 65 | this.name = options.name || ''; 66 | 67 | // this.level is a setter, do this after setting up the factory 68 | this.level = this.options.level ?? 'trace'; 69 | } 70 | 71 | get level(): TLevels[keyof TLevels] { 72 | return this.currentLevel; 73 | } 74 | 75 | get levels(): TLevels { 76 | // eslint-disable-line class-methods-use-this 77 | return this.methodFactory!.levels as unknown as TLevels; 78 | } 79 | 80 | get factory() { 81 | return this.methodFactory as Factory; 82 | } 83 | 84 | set factory(factory) { 85 | // eslint-disable-next-line no-param-reassign 86 | factory.logger = this as any; 87 | this.methodFactory = factory; 88 | this.methodFactory.replaceMethods(this.level as number); 89 | } 90 | 91 | set level(logLevel: SomeLevel) { 92 | const level = this.methodFactory!.distillLevel(logLevel); 93 | 94 | if (level === false || level == null) { 95 | throw new RangeError(`loglevelnext: setLevel() called with invalid level: ${logLevel}`); 96 | } 97 | 98 | this.currentLevel = level; 99 | this.methodFactory!.replaceMethods(level); 100 | 101 | const max = Math.max(...Object.values(this.levels)); 102 | 103 | if (typeof console === 'undefined') { 104 | process.stdout.write('loglevelnext: console is undefined. The log will produce no output.\n'); 105 | } else if (level > max) { 106 | // eslint-disable-next-line no-console 107 | console.warn( 108 | `The log level has been set to a value greater than 'silent'. The log will produce no output.` 109 | ); 110 | } 111 | } 112 | 113 | disable() { 114 | const levels = this.levels; 115 | if (levels.SILENT) { 116 | this.level = levels.SILENT; 117 | } else { 118 | // eslint-disable-next-line no-console 119 | console.warn( 120 | `loglevelnext: no 'silent' level defined. The log cannot be disabled. You may want to override the 'disable' method.` 121 | ); 122 | } 123 | } 124 | 125 | enable() { 126 | const levels = this.levels; 127 | if (typeof levels.TRACE !== 'undefined') { 128 | this.level = levels.TRACE; 129 | } else { 130 | // eslint-disable-next-line no-console 131 | console.warn( 132 | `loglevelnext: no 'trace' level defined. The log cannot be enabled. You may want to override the 'trace' method.` 133 | ); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /docs/LogLevel.md: -------------------------------------------------------------------------------- 1 | # LogLevel 2 | 3 | Every instance of a log in `loglevelnext` is a `LogLevel` class. This reference 4 | applies both to the default logger and all loggers created with `getLogger()`. 5 | 6 | ## Log Levels 7 | 8 | By default `loglevelnext` ships supporting the following log level name-value 9 | pairs: 10 | 11 | ``` 12 | { 13 | TRACE: 0, 14 | DEBUG: 1, 15 | INFO: 2, 16 | WARN: 3, 17 | ERROR: 4, 18 | SILENT: 5 19 | } 20 | ``` 21 | 22 | ## Methods 23 | 24 | ### `constructor(options)` 25 | 26 | The constructor for this class accepts an `options` Object. The following is a 27 | property reference for the Object: 28 | 29 | ### `factory` 30 | 31 | Type: `Class:MethodFactory` 32 | Default: `null` 33 | 34 | Specifies the factory to use when wrapping `console` methods. The value must be 35 | a class which inherits from `MethodFactory`. 36 | 37 | ### `id` 38 | 39 | Type: `String|Any` 40 | 41 | By default, the identifier for a logger is set internally and equal to the 42 | specified logger name. The caching of the loggers works off of this property. 43 | _However_, in some environments or scenarios it may be preferable for no caching 44 | of loggers, resulting in each logger requested being a separate instance. 45 | 46 | _If you require multiple unique loggers of the same name, set this `id` 47 | property to a unique identifier._ 48 | 49 | ### `level` 50 | 51 | Type: `String` 52 | Default: `'info'` 53 | 54 | Specifies the level the logger should use. A logger will not produce output for 55 | any log level _beneath_ the specified level. Available levels and order are: 56 | 57 | ```js 58 | [ 59 | 'trace', 60 | 'debug', 61 | 'info', 62 | 'warn', 63 | 'error', 64 | 'silent' 65 | ] 66 | ``` 67 | 68 | _Note: The level names shown above correspond to the available logging methods, 69 | with the notable exception of the `silent` level._ 70 | 71 | ### `name` 72 | 73 | Type: `String` 74 | Default: `+new Date()` 75 | 76 | Specifies the name of the log to create. This property is required, and used to 77 | differentiate between loggers when `webpack-log` is used in multiple projects 78 | executing in the same process space. 79 | 80 | ### `prefix` 81 | 82 | The `prefix` object defines properties for adding a prefix to each bit of data 83 | logged. 84 | 85 | disable() { 86 | this.level = this.levels.SILENT; 87 | } 88 | 89 | ### `disable` 90 | 91 | Instructs the logger to hide output for every log level. This is equivalent 92 | to setting the `level` property to `'silent'` or `5`. 93 | 94 | ### `enable` 95 | 96 | Instructs the logger to produce output for every log level. This is equivalent 97 | to setting the `level` property to `'trace'` or `0`. 98 | 99 | ### Properties 100 | 101 | #### `level(options)` 102 | 103 | Type: `Function` 104 | Default: ``options => `[${options.level}]` `` 105 | 106 | A function used to define the value used to replace instances of 107 | `{{level}}` within the [prefix template](#template). The `options` argument will 108 | contain an `Object` matching `{ level: (String), logger: (LogLeveL) }`. This 109 | function should represent the current level of log producing output. 110 | 111 | #### `name(options)` 112 | 113 | Type: `Function` 114 | Default: ``options => `[${options.name}]` `` 115 | 116 | A function used to define the value used to replace instances of 117 | `{{name}}` within the [prefix template](#template). The `options` argument will 118 | contain an `Object` matching `{ level: (String), logger: (LogLeveL) }`. This 119 | function should represent the name of the logger producing output. 120 | 121 | #### `template` 122 | 123 | Type: `String` 124 | Default: `'{{time}} {{level}} '` 125 | 126 | Defines the template that represents the prefix. The template uses 127 | [Mustache](https://mustache.github.io) syntax. When prefixing log output, the 128 | logger will interpolate the template string and attempt to execute a function on 129 | the `prefix` object corresponding to a given tag. 130 | 131 | For example: 132 | 133 | ```js 134 | { 135 | prefix: { 136 | foo: () => 'bar', 137 | template: '{{foo}}-' 138 | } 139 | } 140 | ``` 141 | 142 | Would then result in a prefix of `'bar-'` prepended to the output for each log 143 | method call. 144 | 145 | ### `time()` 146 | 147 | Type: `Function` 148 | Default: `() => new Date().toTimeString().split(' ')[0]` 149 | 150 | A function used to define the value used to replace instances of 151 | `{{time}}` within the [prefix template](#template). This function should 152 | represent the time at which the logger produced output. 153 | 154 | ### Properties 155 | 156 | #### `factory` 157 | 158 | Type: `Class:MethodFactory` 159 | 160 | Gets or sets the factory to use when wrapping `console` methods. When setting 161 | this property, the value must be a class which inherits from `MethodFactory`. 162 | 163 | #### `level` 164 | 165 | Type: `String|Number` 166 | 167 | Gets or sets the level the log should operate on. When setting this property, the 168 | value must be a `String` or `Number.` 169 | 170 | #### `levels` 171 | 172 | Type: `Object` 173 | 174 | Gets an object which represents the valid level name-value pairs for this log 175 | instance. 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [tests]: https://img.shields.io/circleci/project/github/shellscape/loglevelnext.svg 2 | [tests-url]: https://circleci.com/gh/shellscape/loglevelnext 3 | [cover]: https://codecov.io/gh/shellscape/loglevelnext/branch/master/graph/badge.svg 4 | [cover-url]: https://codecov.io/gh/shellscape/loglevelnext 5 | [size]: https://packagephobia.now.sh/badge?p=loglevelnext 6 | [size-url]: https://packagephobia.now.sh/result?p=loglevelnext 7 | [loglevel]: https://githhub.com/pimterry/loglevel 8 | [loglevelpre]: https://github.com/kutuluk/loglevel-plugin-prefix 9 | [methodfactory]: lib/factory/MethodFactory.js 10 | [prefixfactory]: lib/factory/PrefixFactory.js 11 | 12 |
13 | 14 |
15 |
16 | 17 | [![tests][tests]][tests-url] 18 | [![cover][cover]][cover-url] 19 | [![size][size]][size-url] 20 | 21 | # loglevelnext 22 | 23 | `loglevelnext` is a modern logging library for Node.js and modern browsers, written with modern patterns and practices which provides log level mapping of the `console` object. 24 | 25 | For browser use, or use in client-side applications, `loglevelnext` should be bundled by your preferred bundler or compiler, such as [Rollup](https://rollupjs.org). 26 | 27 | ## Getting Started 28 | 29 | First thing's first, install the module: 30 | 31 | ```console 32 | npm install loglevelnext --save 33 | ``` 34 | 35 | ## Usage 36 | 37 | Users can choose to use `loglevelnext` in Node.js or in the client (browser). 38 | 39 | ```js 40 | const log = require('loglevelnext'); 41 | 42 | log.info('bananas!'); 43 | ``` 44 | 45 | ## Log Levels 46 | 47 | By default `loglevelnext` ships supporting the following log level name-value 48 | pairs: 49 | 50 | ```js 51 | { 52 | TRACE: 0, 53 | DEBUG: 1, 54 | INFO: 2, 55 | WARN: 3, 56 | ERROR: 4, 57 | SILENT: 5 58 | } 59 | ``` 60 | 61 | ## Default Logger 62 | 63 | When requiring `loglevelnext` in Node.js the default export will be an instance of [`LogLevel`](docs/LogLevel.md) wrapped with some extra sugar. 64 | 65 | ### Methods 66 | 67 | Please see [`LogLevel`](docs/LogLevel.md) for documentation of all methods and properties of every log instance, including the default instance. 68 | 69 | #### `trace`, `debug`, `info`, `warn`, `error` 70 | 71 | These methods correspond to the available log levels and accept parameters identical to their `console` counterparts. e.g. 72 | 73 | ```js 74 | console.info('...'); 75 | console.info('...'); 76 | // ... etc 77 | ``` 78 | 79 | #### `create(options)` 80 | 81 | Returns a new `LogLevel` instance. The `options` parameter should be an `Object` matching the options for the [`LogLevel`](docs/LogLevel.md) constructor. 82 | 83 | _Note: `LogLevel` instances created are cached. Calling `create` with a previously used `name` will return the cached `LogLevel` instance. To create a different instance with the same `name`, assign a unique `id` property to the `options` parameter._ 84 | 85 | ### Properties 86 | 87 | #### `factories` 88 | 89 | Type: `Array [ Class ]` 90 | 91 | Returns an `Array` containing the factory classes available within `loglevelnext` 92 | to outside modules. Particularly useful when creating plugins. eg. 93 | 94 | ```js 95 | const log = require('loglevelnext'); 96 | const { MethodFactory } = log.factories; 97 | class MyFactory extends MethodFactory { ... } 98 | ``` 99 | 100 | #### `loggers` 101 | 102 | Type: `Array [ LogLevel ]` 103 | 104 | Returns an `Array` containing references to the currently instantiated loggers. 105 | 106 | ## Factories aka Plugins 107 | 108 | If you're used to using plugins with `loglevel`, fear not. The same capabilities 109 | are available in `loglevelnext`, but in a much more straightforward and structured 110 | way. `loglevelnext` supports by way of "Factories." A `Factory` is nothing more 111 | than a class which defines several base methods that operate on the `console` 112 | and provide functionality to a `LogLevel` instance. All factories must inherit from the 113 | [`MethodFactory`][methodfactory] class, and may override any defined class functions. 114 | 115 | For an example factory, please have a look at the [`PrefixFactory`][prefixfactory] 116 | which provides similar functionality as the [loglevel-prefix](loglevelpre) plugin, 117 | and is the factory which is used when a user passes the `prefix` option to a 118 | `LogLevel` instance. 119 | 120 | ## Browser Support 121 | 122 | As mentioned, `loglevelnext` is a logging library for Node.js and _modern_ browsers, which means the latest versions of the major browsers. When bundling or compiling `loglevelnext` for use in a browser, you should ensure that appropriate polyfills are used. e.g. Internet Explorer typically requires polyfilling both `Symbol` and `Object.assign`. 123 | 124 | ## Attribution 125 | 126 | _This project originated as a fork of the much-loved [loglevel](loglevel) module, but has diverged and has been rewritten, and now shares similarities only in functional intent._ 127 | 128 | Base Log SVG by [Freepik](http://www.freepik.com/) from [www.flaticon.com](http://www.flaticon.com). 129 | 130 | ## Meta 131 | 132 | [CONTRIBUTING](./.github/CONTRIBUTING) 133 | 134 | [LICENSE (Mozilla Public License)](./LICENSE) 135 | -------------------------------------------------------------------------------- /src/MethodFactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | 12 | import { LogLevel } from './LogLevel'; 13 | 14 | interface BindTarget { 15 | [key: string]: Function; 16 | } 17 | 18 | export type FactoryLevels = Record, number> & { 19 | DEBUG: number; 20 | ERROR: number; 21 | INFO: number; 22 | SILENT: number; 23 | TRACE: number; 24 | WARN: number; 25 | }; 26 | 27 | /* eslint-disable sort-keys */ 28 | export const defaultLevels = { 29 | TRACE: 0, 30 | DEBUG: 1, 31 | INFO: 2, 32 | WARN: 3, 33 | ERROR: 4, 34 | SILENT: 5 35 | } as FactoryLevels; 36 | /* eslint-enable sort-keys */ 37 | 38 | const levels = Symbol('log-levels'); 39 | const instance = Symbol('log-instance'); 40 | 41 | export interface Factory { 42 | [key: string]: any; 43 | bindMethod: (obj: BindTarget, methodName: string) => any; 44 | distillLevel: (level: number | string) => any; 45 | [instance]: LogLevel | undefined; 46 | levelValid: (level: number) => boolean; 47 | [levels]: TLevels; 48 | levels: TLevels; 49 | logger: LogLevel; 50 | make: (methodName: string) => Function; 51 | methods?: string[]; 52 | 53 | replaceMethods: (logLevel: number | string) => void; 54 | } 55 | 56 | const noop = () => {}; 57 | 58 | export type MethodFactoryLevels = Lowercase; 59 | 60 | export class MethodFactory 61 | implements Factory 62 | { 63 | public [instance]: LogLevel | undefined; 64 | public [levels]: TLevels; 65 | 66 | constructor(logger?: LogLevel) { 67 | this[instance] = logger; 68 | this[levels] = defaultLevels as any; 69 | } 70 | 71 | // @ts-ignore 72 | get levels(): TLevels { 73 | return (this as any)[levels]; 74 | } 75 | 76 | get logger(): LogLevel { 77 | return (this as any)[instance]; 78 | } 79 | 80 | get methods() { 81 | return Object.keys(this.levels) 82 | .map((key) => key.toLowerCase()) 83 | .filter((key) => key !== 'silent'); 84 | } 85 | 86 | set logger(logger: LogLevel) { 87 | (this as any)[instance] = logger; 88 | } 89 | 90 | // eslint-disable-next-line class-methods-use-this 91 | bindMethod(obj: BindTarget, methodName: string) { 92 | const method = obj[methodName]; 93 | if (typeof method.bind === 'function') { 94 | return method.bind(obj); 95 | } 96 | 97 | try { 98 | return Function.prototype.bind.call(method, obj); 99 | } catch (e) { 100 | // Missing bind shim or IE8 + Modernizr, fallback to wrapping 101 | return function result() { 102 | // eslint-disable-next-line prefer-rest-params 103 | return Function.prototype.apply.apply(method, [obj, arguments]); 104 | }; 105 | } 106 | } 107 | 108 | distillLevel(level: number | string) { 109 | let value; 110 | 111 | if (typeof level === 'string') { 112 | const levels = this.levels; 113 | value = levels[level.toUpperCase() as Uppercase]; 114 | } else { 115 | value = level; 116 | } 117 | 118 | if (this.levelValid(value)) return value; 119 | 120 | return null; 121 | } 122 | 123 | levelValid(level: number) { 124 | const max = Math.max(...Object.values(this.levels)); 125 | if (typeof level === 'number' && level >= 0 && level <= max) { 126 | return true; 127 | } 128 | 129 | return false; 130 | } 131 | 132 | /** 133 | * Build the best logging method possible for this env 134 | * Wherever possible we want to bind, not wrap, to preserve stack traces. 135 | * Since we're targeting modern browsers, there's no need to wait for the 136 | * console to become available. 137 | */ 138 | // eslint-disable-next-line class-methods-use-this 139 | make(methodName: string): Function { 140 | /* eslint-disable no-console */ 141 | const target = console as unknown as BindTarget; 142 | if (typeof target[methodName] !== 'undefined') { 143 | return this.bindMethod(target, methodName); 144 | } else if (typeof console.log !== 'undefined') { 145 | return this.bindMethod(target, 'log'); 146 | } 147 | 148 | /* eslint-enable no-console */ 149 | return noop; 150 | } 151 | 152 | replaceMethods(logLevel: number | string) { 153 | const level = this.distillLevel(logLevel); 154 | 155 | if (level === null) { 156 | throw new Error(`loglevelnext: replaceMethods() called with invalid level: ${logLevel}`); 157 | } 158 | 159 | if (!this.logger || this.logger.type !== 'LogLevel') { 160 | throw new TypeError( 161 | 'loglevelnext: Logger is undefined or invalid. Please specify a valid Logger instance.' 162 | ); 163 | } 164 | 165 | this.methods.forEach((methodName) => { 166 | // @ts-ignore 167 | const { [methodName.toUpperCase()]: methodLevel } = this.levels; 168 | 169 | this.logger[methodName] = methodLevel < level ? noop : this.make(methodName); 170 | }); 171 | 172 | // Define log.log as an alias for log.debug 173 | this.logger.log = this.logger.debug; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /assets/loglevelnext-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 30 | 31 | 36 | 37 | 39 | 40 | 41 | 49 | 61 | 66 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 87 | 102 | 103 | 104 | 106 | 114 | 118 | 126 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Andrew Powell 2 | 3 | Mozilla Public License Version 2.0 4 | ================================== 5 | 6 | 1. Definitions 7 | -------------- 8 | 9 | 1.1. "Contributor" 10 | means each individual or legal entity that creates, contributes to 11 | the creation of, or owns Covered Software. 12 | 13 | 1.2. "Contributor Version" 14 | means the combination of the Contributions of others (if any) used 15 | by a Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | means Covered Software of a particular Contributor. 19 | 20 | 1.4. "Covered Software" 21 | means Source Code Form to which the initial Contributor has attached 22 | the notice in Exhibit A, the Executable Form of such Source Code 23 | Form, and Modifications of such Source Code Form, in each case 24 | including portions thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | (a) that the initial Contributor has attached the notice described 30 | in Exhibit B to the Covered Software; or 31 | 32 | (b) that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the 34 | terms of a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | means any form of the work other than Source Code Form. 38 | 39 | 1.7. "Larger Work" 40 | means a work that combines Covered Software with other material, in 41 | a separate file or files, that is not Covered Software. 42 | 43 | 1.8. "License" 44 | means this document. 45 | 46 | 1.9. "Licensable" 47 | means having the right to grant, to the maximum extent possible, 48 | whether at the time of the initial grant or subsequently, any and 49 | all of the rights conveyed by this License. 50 | 51 | 1.10. "Modifications" 52 | means any of the following: 53 | 54 | (a) any file in Source Code Form that results from an addition to, 55 | deletion from, or modification of the contents of Covered 56 | Software; or 57 | 58 | (b) any new file in Source Code Form that contains any Covered 59 | Software. 60 | 61 | 1.11. "Patent Claims" of a Contributor 62 | means any patent claim(s), including without limitation, method, 63 | process, and apparatus claims, in any patent Licensable by such 64 | Contributor that would be infringed, but for the grant of the 65 | License, by the making, using, selling, offering for sale, having 66 | made, import, or transfer of either its Contributions or its 67 | Contributor Version. 68 | 69 | 1.12. "Secondary License" 70 | means either the GNU General Public License, Version 2.0, the GNU 71 | Lesser General Public License, Version 2.1, the GNU Affero General 72 | Public License, Version 3.0, or any later versions of those 73 | licenses. 74 | 75 | 1.13. "Source Code Form" 76 | means the form of the work preferred for making modifications. 77 | 78 | 1.14. "You" (or "Your") 79 | means an individual or a legal entity exercising rights under this 80 | License. For legal entities, "You" includes any entity that 81 | controls, is controlled by, or is under common control with You. For 82 | purposes of this definition, "control" means (a) the power, direct 83 | or indirect, to cause the direction or management of such entity, 84 | whether by contract or otherwise, or (b) ownership of more than 85 | fifty percent (50%) of the outstanding shares or beneficial 86 | ownership of such entity. 87 | 88 | 2. License Grants and Conditions 89 | -------------------------------- 90 | 91 | 2.1. Grants 92 | 93 | Each Contributor hereby grants You a world-wide, royalty-free, 94 | non-exclusive license: 95 | 96 | (a) under intellectual property rights (other than patent or trademark) 97 | Licensable by such Contributor to use, reproduce, make available, 98 | modify, display, perform, distribute, and otherwise exploit its 99 | Contributions, either on an unmodified basis, with Modifications, or 100 | as part of a Larger Work; and 101 | 102 | (b) under Patent Claims of such Contributor to make, use, sell, offer 103 | for sale, have made, import, and otherwise transfer either its 104 | Contributions or its Contributor Version. 105 | 106 | 2.2. Effective Date 107 | 108 | The licenses granted in Section 2.1 with respect to any Contribution 109 | become effective for each Contribution on the date the Contributor first 110 | distributes such Contribution. 111 | 112 | 2.3. Limitations on Grant Scope 113 | 114 | The licenses granted in this Section 2 are the only rights granted under 115 | this License. No additional rights or licenses will be implied from the 116 | distribution or licensing of Covered Software under this License. 117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 118 | Contributor: 119 | 120 | (a) for any code that a Contributor has removed from Covered Software; 121 | or 122 | 123 | (b) for infringements caused by: (i) Your and any other third party's 124 | modifications of Covered Software, or (ii) the combination of its 125 | Contributions with other software (except as part of its Contributor 126 | Version); or 127 | 128 | (c) under Patent Claims infringed by Covered Software in the absence of 129 | its Contributions. 130 | 131 | This License does not grant any rights in the trademarks, service marks, 132 | or logos of any Contributor (except as may be necessary to comply with 133 | the notice requirements in Section 3.4). 134 | 135 | 2.4. Subsequent Licenses 136 | 137 | No Contributor makes additional grants as a result of Your choice to 138 | distribute the Covered Software under a subsequent version of this 139 | License (see Section 10.2) or under the terms of a Secondary License (if 140 | permitted under the terms of Section 3.3). 141 | 142 | 2.5. Representation 143 | 144 | Each Contributor represents that the Contributor believes its 145 | Contributions are its original creation(s) or it has sufficient rights 146 | to grant the rights to its Contributions conveyed by this License. 147 | 148 | 2.6. Fair Use 149 | 150 | This License is not intended to limit any rights You have under 151 | applicable copyright doctrines of fair use, fair dealing, or other 152 | equivalents. 153 | 154 | 2.7. Conditions 155 | 156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 157 | in Section 2.1. 158 | 159 | 3. Responsibilities 160 | ------------------- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | --------------------------------------------------- 223 | 224 | If it is impossible for You to comply with any of the terms of this 225 | License with respect to some or all of the Covered Software due to 226 | statute, judicial order, or regulation then You must: (a) comply with 227 | the terms of this License to the maximum extent possible; and (b) 228 | describe the limitations and the code they affect. Such description must 229 | be placed in a text file included with all distributions of the Covered 230 | Software under this License. Except to the extent prohibited by statute 231 | or regulation, such description must be sufficiently detailed for a 232 | recipient of ordinary skill to be able to understand it. 233 | 234 | 5. Termination 235 | -------------- 236 | 237 | 5.1. The rights granted under this License will terminate automatically 238 | if You fail to comply with any of its terms. However, if You become 239 | compliant, then the rights granted under this License from a particular 240 | Contributor are reinstated (a) provisionally, unless and until such 241 | Contributor explicitly and finally terminates Your grants, and (b) on an 242 | ongoing basis, if such Contributor fails to notify You of the 243 | non-compliance by some reasonable means prior to 60 days after You have 244 | come back into compliance. Moreover, Your grants from a particular 245 | Contributor are reinstated on an ongoing basis if such Contributor 246 | notifies You of the non-compliance by some reasonable means, this is the 247 | first time You have received notice of non-compliance with this License 248 | from such Contributor, and You become compliant prior to 30 days after 249 | Your receipt of the notice. 250 | 251 | 5.2. If You initiate litigation against any entity by asserting a patent 252 | infringement claim (excluding declaratory judgment actions, 253 | counter-claims, and cross-claims) alleging that a Contributor Version 254 | directly or indirectly infringes any patent, then the rights granted to 255 | You by any and all Contributors for the Covered Software under Section 256 | 2.1 of this License shall terminate. 257 | 258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 259 | end user license agreements (excluding distributors and resellers) which 260 | have been validly granted by You or Your distributors under this License 261 | prior to termination shall survive termination. 262 | 263 | ************************************************************************ 264 | * * 265 | * 6. Disclaimer of Warranty * 266 | * ------------------------- * 267 | * * 268 | * Covered Software is provided under this License on an "as is" * 269 | * basis, without warranty of any kind, either expressed, implied, or * 270 | * statutory, including, without limitation, warranties that the * 271 | * Covered Software is free of defects, merchantable, fit for a * 272 | * particular purpose or non-infringing. The entire risk as to the * 273 | * quality and performance of the Covered Software is with You. * 274 | * Should any Covered Software prove defective in any respect, You * 275 | * (not any Contributor) assume the cost of any necessary servicing, * 276 | * repair, or correction. This disclaimer of warranty constitutes an * 277 | * essential part of this License. No use of any Covered Software is * 278 | * authorized under this License except under this disclaimer. * 279 | * * 280 | ************************************************************************ 281 | 282 | ************************************************************************ 283 | * * 284 | * 7. Limitation of Liability * 285 | * -------------------------- * 286 | * * 287 | * Under no circumstances and under no legal theory, whether tort * 288 | * (including negligence), contract, or otherwise, shall any * 289 | * Contributor, or anyone who distributes Covered Software as * 290 | * permitted above, be liable to You for any direct, indirect, * 291 | * special, incidental, or consequential damages of any character * 292 | * including, without limitation, damages for lost profits, loss of * 293 | * goodwill, work stoppage, computer failure or malfunction, or any * 294 | * and all other commercial damages or losses, even if such party * 295 | * shall have been informed of the possibility of such damages. This * 296 | * limitation of liability shall not apply to liability for death or * 297 | * personal injury resulting from such party's negligence to the * 298 | * extent applicable law prohibits such limitation. Some * 299 | * jurisdictions do not allow the exclusion or limitation of * 300 | * incidental or consequential damages, so this exclusion and * 301 | * limitation may not apply to You. * 302 | * * 303 | ************************************************************************ 304 | 305 | 8. Litigation 306 | ------------- 307 | 308 | Any litigation relating to this License may be brought only in the 309 | courts of a jurisdiction where the defendant maintains its principal 310 | place of business and such litigation shall be governed by laws of that 311 | jurisdiction, without reference to its conflict-of-law provisions. 312 | Nothing in this Section shall prevent a party's ability to bring 313 | cross-claims or counter-claims. 314 | 315 | 9. Miscellaneous 316 | ---------------- 317 | 318 | This License represents the complete agreement concerning the subject 319 | matter hereof. If any provision of this License is held to be 320 | unenforceable, such provision shall be reformed only to the extent 321 | necessary to make it enforceable. Any law or regulation which provides 322 | that the language of a contract shall be construed against the drafter 323 | shall not be used to construe this License against a Contributor. 324 | 325 | 10. Versions of the License 326 | --------------------------- 327 | 328 | 10.1. New Versions 329 | 330 | Mozilla Foundation is the license steward. Except as provided in Section 331 | 10.3, no one other than the license steward has the right to modify or 332 | publish new versions of this License. Each version will be given a 333 | distinguishing version number. 334 | 335 | 10.2. Effect of New Versions 336 | 337 | You may distribute the Covered Software under the terms of the version 338 | of the License under which You originally received the Covered Software, 339 | or under the terms of any subsequent version published by the license 340 | steward. 341 | 342 | 10.3. Modified Versions 343 | 344 | If you create software not governed by this License, and you want to 345 | create a new license for such software, you may create and use a 346 | modified version of this License if you rename the license and remove 347 | any references to the name of the license steward (except to note that 348 | such modified license differs from this License). 349 | 350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 351 | Licenses 352 | 353 | If You choose to distribute Source Code Form that is Incompatible With 354 | Secondary Licenses under the terms of this version of the License, the 355 | notice described in Exhibit B of this License must be attached. 356 | 357 | Exhibit A - Source Code Form License Notice 358 | ------------------------------------------- 359 | 360 | This Source Code Form is subject to the terms of the Mozilla Public 361 | License, v. 2.0. If a copy of the MPL was not distributed with this 362 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 363 | 364 | If it is not possible or desirable to put the notice in a particular 365 | file, then You may include the notice in a location (such as a LICENSE 366 | file in a relevant directory) where a recipient would be likely to look 367 | for such a notice. 368 | 369 | You may add additional accurate notices of copyright ownership. 370 | 371 | Exhibit B - "Incompatible With Secondary Licenses" Notice 372 | --------------------------------------------------------- 373 | 374 | This Source Code Form is "Incompatible With Secondary Licenses", as 375 | defined by the Mozilla Public License, v. 2.0. 376 | --------------------------------------------------------------------------------