├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── COMMIT_CONVENTION.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── lock.yml └── stale.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── config.json ├── example └── index.ts ├── fancy-logs.png ├── index.ts ├── japaFile.js ├── package-lock.json ├── package.json ├── src ├── Logger.ts └── contracts.ts ├── test └── logger.spec.ts ├── tsconfig.json └── typedoc.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build_12.18.2: 4 | docker: 5 | - image: 'circleci/node:12.18.2' 6 | working_directory: ~/app 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - 'v1-dependencies-{{ checksum "package.json" }}' 12 | - v1-dependencies- 13 | - run: npm install 14 | - save_cache: 15 | paths: 16 | - node_modules 17 | key: 'v1-dependencies-{{ checksum "package.json" }}' 18 | - run: npm test 19 | build_latest: 20 | docker: 21 | - image: 'circleci/node:latest' 22 | working_directory: ~/app 23 | steps: 24 | - checkout 25 | - restore_cache: 26 | keys: 27 | - 'v1-dependencies-{{ checksum "package.json" }}' 28 | - v1-dependencies- 29 | - run: npm install 30 | - save_cache: 31 | paths: 32 | - node_modules 33 | key: 'v1-dependencies-{{ checksum "package.json" }}' 34 | - run: npm test 35 | workflows: 36 | version: 2 37 | workflow: 38 | jobs: 39 | - build_12.18.2 40 | - build_latest 41 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = tab 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:adonis/typescriptPackage", 4 | "prettier", 5 | "prettier/@typescript-eslint" 6 | ], 7 | "plugins": [ 8 | "prettier" 9 | ], 10 | "rules": { 11 | "prettier/prettier": [ 12 | "error" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ```js 8 | ;/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | 13 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 14 | 15 | > The **scope** is optional 16 | 17 | ``` 18 | feat(router): add support for prefix 19 | 20 | Prefix makes it easier to append a path to a group of routes 21 | ``` 22 | 23 | 1. `feat` is type. 24 | 2. `router` is scope and is optional 25 | 3. `add support for prefix` is the subject 26 | 4. The **body** is followed by a blank line. 27 | 5. The optional **footer** can be added after the body, followed by a blank line. 28 | 29 | ## Types 30 | 31 | Only one type can be used at a time and only following types are allowed. 32 | 33 | - feat 34 | - fix 35 | - docs 36 | - style 37 | - refactor 38 | - perf 39 | - test 40 | - workflow 41 | - ci 42 | - chore 43 | - types 44 | - build 45 | 46 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 47 | 48 | ### Revert 49 | 50 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 51 | 52 | ## Scope 53 | 54 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 55 | 56 | ## Subject 57 | 58 | The subject contains succinct description of the change: 59 | 60 | - use the imperative, present tense: "change" not "changed" nor "changes". 61 | - don't capitalize first letter 62 | - no dot (.) at the end 63 | 64 | ## Body 65 | 66 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 67 | The body should include the motivation for the change and contrast this with previous behavior. 68 | 69 | ## Footer 70 | 71 | The footer should contain any information about **Breaking Changes** and is also the place to 72 | reference GitHub issues that this commit **Closes**. 73 | 74 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 75 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Prerequisites 4 | 5 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 6 | 7 | - Ensure the issue isn't already reported. 8 | - Ensure you are reporting the bug in the correct repo. 9 | 10 | _Delete the above section and the instructions in the sections below before submitting_ 11 | 12 | ## Description 13 | 14 | If this is a feature request, explain why it should be added. Specific use-cases are best. 15 | 16 | For bug reports, please provide as much _relevant_ info as possible. 17 | 18 | ## Package version 19 | 20 | 21 | 22 | ## Error Message & Stack Trace 23 | 24 | ## Relevant Information 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/poppinss/fancy-logs/blob/master/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: ['Type: Security'] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: false 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - 'Type: Security' 10 | 11 | # Label to use when marking an issue as stale 12 | staleLabel: 'Status: Abandoned' 13 | 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | yarn.lock 13 | shrinkwrap.yaml 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message="chore(release): %s" 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | config.json 4 | .eslintrc.json 5 | package.json 6 | *.html 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "useTabs": true, 6 | "quoteProps": "consistent", 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "printWidth": 100, 10 | "overrides": [ 11 | { 12 | "files": "*.md", 13 | "options": { 14 | "useTabs": false 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests. And following this guidelines will make your pull request easier to merge 4 | 5 | ## Prerequisites 6 | 7 | - Install [EditorConfig](http://editorconfig.org/) plugin for your code editor to make sure it uses correct settings. 8 | - Fork the repository and clone your fork. 9 | - Install dependencies: `npm install`. 10 | 11 | ## Coding style 12 | 13 | We make use of Typescript along with [Tslint](https://palantir.github.io/tslint) to ensure a consistent coding style. All of the rules are defined inside the `tslint.json` file. 14 | 15 | ## Development work-flow 16 | 17 | Always make sure to lint and test your code before pushing it to the GitHub. 18 | 19 | ```bash 20 | npm test 21 | ``` 22 | 23 | Just lint the code 24 | 25 | ```bash 26 | npm run lint 27 | ``` 28 | 29 | **Make sure you add sufficient tests for the change**. 30 | 31 | ## Other notes 32 | 33 | - Do not change version number inside the `package.json` file. 34 | - Do not update `CHANGELOG.md` file. 35 | - Do not update `tslint.json` or `tslint.js` file. If something prevents you writing code, please create an issue for same. 36 | 37 | ## Need help? 38 | 39 | Feel free to ask. 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2019 Harminder virk, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fancy Logger 2 | 3 | > Fancy logger used by AdonisJS CLI apps 4 | 5 | [![circleci-image]][circleci-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] 6 | 7 | Fancy logger for logging colorful messages with consistent UI output. AdonisJs has various command line utilities including [ace](https://github.com/adonisjs/ace). (A framework to create CLI applications). We make use of this module to make sure that all parts of the framework output logs with consistent formatting. 8 | 9 | A big thanks to the creator of [signale](https://github.com/klaussinani/signale) for being an inspiration for this module. 10 | 11 | We didn't used signale coz of following reasons: 12 | 13 | 1. AdonisJs uses kleur for colorizing strings and signale uses chalk. We want to avoid loading different color libraries. 14 | 2. Signale has too many features that we don't need. 15 | 16 | 17 | 18 | ## Table of contents 19 | 20 | - [Installation](#installation) 21 | - [Usage](#usage) 22 | - [Using a custom instance](#using-a-custom-instance) 23 | - [Deferred logs](#deferred-logs) 24 | - [Testing log messages](#testing-log-messages) 25 | 26 | 27 | 28 | ## Installation 29 | 30 | Install the package from npm registry as follows: 31 | 32 | ```sh 33 | npm i @poppinss/fancy-logs 34 | 35 | # yarn 36 | yarn add @poppinss/fancy-logs 37 | ``` 38 | 39 | ## Usage 40 | 41 | Import and use the logger as follows 42 | 43 | ```ts 44 | import fancyLogger from '@poppinss/fancy-logs' 45 | 46 | fancyLogger.success('Operation successful') 47 | fancyLogger.info('Hello from L59') 48 | fancyLogger.pending('Write release notes for %s', '1.2.0') 49 | fancyLogger.fatal(new Error('Unable to acquire lock')) 50 | fancyLogger.watch('Recursively watching build directory...') 51 | fancyLogger.complete({ 52 | prefix: '[task]', 53 | message: 'Fix issue #59', 54 | }) 55 | ``` 56 | 57 | ![](./fancy-logs.png) 58 | 59 | ## Using a custom instance 60 | 61 | ```ts 62 | import { Logger } from '@poppinss/fancy-logs' 63 | 64 | /** 65 | * Disable underlines and icons 66 | */ 67 | const fancyLogger = new Logger({ 68 | underline: false, 69 | icon: false, 70 | color: true, 71 | }) 72 | ``` 73 | 74 | ## Deferred logs 75 | 76 | When running CLI tasks from 3rd party plugins, you may end in a situation where multiple plugins will print the same log messages. For example: 77 | 78 | **Plugin A updates `tsconfig.json`** 79 | 80 | ``` 81 | logger.update('tsconfig.json') 82 | ``` 83 | 84 | **Plugin B update `tsconfig.json` with a different option but logs the same message** 85 | 86 | ``` 87 | logger.update('tsconfig.json') 88 | ``` 89 | 90 | After this the CLI will reflect 2 lines saying `update tsconfig.json`. You can avoid this behavior by pausing and resuming the logger. 91 | 92 | ```ts 93 | logger.pauseLogger() 94 | 95 | runPluginA() 96 | runPluginB() 97 | 98 | const logsSet = new Set() 99 | logger.resumeLogger((message) => { 100 | if (logsSet.has(message.message)) { 101 | return false 102 | } 103 | 104 | logsSet.add(message.message) 105 | return true 106 | }) 107 | ``` 108 | 109 | The callback passed to `resumeLogger` must return `true` when it wants to print a message and `false` for opposite behavior. 110 | 111 | ## Testing log messages 112 | 113 | You can also safely test the log messages by creating an instance with `fake=true`. For example: 114 | 115 | ```ts 116 | import { Logger } from '@poppinss/fancy-logs' 117 | 118 | const logger = new Logger({ fake: true }) 119 | 120 | logger.warn('Fire in the hole') 121 | logger.info('Account created') 122 | 123 | assert.deepEqual(logger.logs, [ 124 | 'underline(yellow(warn)) Fire in the hole', 125 | 'underline(blue(info)) Account created', 126 | ]) 127 | ``` 128 | 129 | [circleci-image]: https://img.shields.io/circleci/project/github/poppinss/fancy-logs/master.svg?style=for-the-badge&logo=circleci 130 | [circleci-url]: https://circleci.com/gh/poppinss/fancy-logs 'circleci' 131 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 132 | [typescript-url]: "typescript" 133 | [npm-image]: https://img.shields.io/npm/v/@poppinss/fancy-logs.svg?style=for-the-badge&logo=npm 134 | [npm-url]: https://npmjs.org/package/@poppinss/fancy-logs 'npm' 135 | [license-image]: https://img.shields.io/npm/l/@poppinss/fancy-logs?color=blueviolet&style=for-the-badge 136 | [license-url]: LICENSE.md 'license' 137 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": false, 3 | "ts": true, 4 | "license": "MIT", 5 | "services": [ 6 | "circleci" 7 | ], 8 | "minNodeVersion": "12.18.2", 9 | "probotApps": [ 10 | "stale", 11 | "lock" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import fancyLogger, { Logger } from '../index' 2 | 3 | fancyLogger.success('Operation successful') 4 | fancyLogger.info('Hello from L59') 5 | fancyLogger.pending('Write release notes for %s', '1.2.0') 6 | fancyLogger.fatal(new Error('Unable to acquire lock')) 7 | fancyLogger.watch('Recursively watching build directory...') 8 | fancyLogger.warn('fire in the hole') 9 | fancyLogger.complete({ 10 | prefix: '[task]', 11 | message: 'Fix issue #59', 12 | }) 13 | 14 | fancyLogger.success({ message: 'Operation successful', icon: false }) 15 | fancyLogger.compile({ message: 'Operation successful' }) 16 | fancyLogger.info({ message: 'installing dependencies', suffix: '(npm)' }) 17 | fancyLogger.info({ message: 'installing dependencies', color: false, icon: false }) 18 | fancyLogger.skip({ message: 'creating new file' }) 19 | fancyLogger.warn({ message: 'fire in the hole', icon: false }) 20 | 21 | fancyLogger.fatal({ message: new Error('Unable to acquire lock'), icon: false }) 22 | 23 | console.log('\n======= CUSTOM LOGGER ==========\n') 24 | 25 | const customLogger = new Logger({ underline: false, icon: false, color: false }) 26 | customLogger.success('Operation successful') 27 | customLogger.info('Hello from L59') 28 | customLogger.pending('Write release notes for %s', '1.2.0') 29 | customLogger.fatal(new Error('Unable to acquire lock')) 30 | customLogger.watch('Recursively watching build directory...') 31 | customLogger.complete({ 32 | prefix: '[task]', 33 | message: 'Fix issue #59', 34 | }) 35 | 36 | customLogger.success({ message: 'Operation successful', icon: true }) 37 | customLogger.compile({ message: 'Operation successful' }) 38 | customLogger.info({ message: 'installing dependencies', suffix: '(npm)' }) 39 | customLogger.info({ message: 'installing dependencies', color: true, icon: true }) 40 | 41 | customLogger.fatal({ message: new Error('Unable to acquire lock'), icon: true }) 42 | customLogger.skip({ message: 'creating new file' }) 43 | 44 | console.log('\n======= PAUSING LOGGER TO AVOID DUPLICATES ==========\n') 45 | 46 | const duplicates: Set = new Set() 47 | 48 | fancyLogger.pauseLogger() 49 | fancyLogger.success('Operation successful') 50 | fancyLogger.info('Hello from L59') 51 | fancyLogger.success('Operation successful') 52 | fancyLogger.pending('Write release notes for %s', '1.2.0') 53 | fancyLogger.fatal(new Error('Unable to acquire lock')) 54 | fancyLogger.watch('Recursively watching build directory...') 55 | fancyLogger.fatal(new Error('Unable to acquire lock')) 56 | fancyLogger.info({ message: 'installing dependencies', color: false, icon: false }) 57 | fancyLogger.info({ message: 'installing dependencies', color: false, icon: false }) 58 | 59 | fancyLogger.resumeLogger((log) => { 60 | if (log.action === 'fatal') { 61 | return true 62 | } 63 | 64 | if (duplicates.has(log.message)) { 65 | return false 66 | } 67 | 68 | duplicates.add(log.message) 69 | return true 70 | }) 71 | -------------------------------------------------------------------------------- /fancy-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poppinss/fancy-logs/271362ac3a332703293d0abc55034e24484bb518/fancy-logs.png -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @poppinss/fancy-logs 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Logger } from './src/Logger' 11 | 12 | const logger = new Logger() 13 | export default logger 14 | export { Logger } 15 | -------------------------------------------------------------------------------- /japaFile.js: -------------------------------------------------------------------------------- 1 | require('@adonisjs/require-ts/build/register') 2 | 3 | const { configure } = require('japa') 4 | configure({ 5 | files: ['test/**/*.spec.ts'], 6 | }) 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@poppinss/fancy-logs", 3 | "version": "1.3.9", 4 | "description": "Printing fancy logs to the terminal", 5 | "main": "build/index.js", 6 | "files": [ 7 | "build/src", 8 | "build/index.d.ts", 9 | "build/index.js" 10 | ], 11 | "scripts": { 12 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 13 | "pretest": "npm run lint", 14 | "test": "node japaFile.js", 15 | "lint": "eslint . --ext=.ts", 16 | "clean": "del build", 17 | "compile": "npm run lint && npm run clean && tsc", 18 | "build": "npm run compile", 19 | "commit": "git-cz", 20 | "release": "np", 21 | "version": "npm run build", 22 | "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json poppinss/fancy-logs", 23 | "format": "prettier --write ." 24 | }, 25 | "author": "virk,poppinss", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@adonisjs/mrm-preset": "^2.4.0", 29 | "@adonisjs/require-ts": "^1.0.0", 30 | "@types/node": "^14.11.1", 31 | "commitizen": "^4.2.1", 32 | "cz-conventional-changelog": "^3.3.0", 33 | "del-cli": "^3.0.1", 34 | "doctoc": "^1.4.0", 35 | "eslint": "^7.9.0", 36 | "eslint-config-prettier": "^6.11.0", 37 | "eslint-plugin-adonis": "^1.0.15", 38 | "eslint-plugin-prettier": "^3.1.4", 39 | "github-label-sync": "^2.0.0", 40 | "husky": "^4.3.0", 41 | "japa": "^3.1.1", 42 | "mrm": "^2.5.0", 43 | "np": "^6.5.0", 44 | "prettier": "^2.1.2", 45 | "typescript": "^4.0.3" 46 | }, 47 | "nyc": { 48 | "exclude": [ 49 | "test" 50 | ], 51 | "extension": [ 52 | ".ts" 53 | ] 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": "doctoc README.md --title='## Table of contents' && git add README.md", 58 | "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" 59 | } 60 | }, 61 | "config": { 62 | "commitizen": { 63 | "path": "cz-conventional-changelog" 64 | } 65 | }, 66 | "dependencies": { 67 | "@poppinss/colors": "^2.1.1", 68 | "figures": "^3.2.0", 69 | "string-width": "^4.2.2" 70 | }, 71 | "directories": { 72 | "doc": "docs", 73 | "example": "example", 74 | "test": "test" 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "url": "git+https://github.com/poppinss/fancy-logs.git" 79 | }, 80 | "keywords": [ 81 | "logger", 82 | "fancy-logs" 83 | ], 84 | "bugs": { 85 | "url": "https://github.com/poppinss/fancy-logs/issues" 86 | }, 87 | "homepage": "https://github.com/poppinss/fancy-logs#readme", 88 | "np": { 89 | "contents": ".", 90 | "anyBranch": false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @poppinss/fancy-logs 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import figures from 'figures' 11 | import { format } from 'util' 12 | import stringWidth from 'string-width' 13 | import { Colors, FakeColors } from '@poppinss/colors' 14 | import { ActionsList, MessageNode, DeferredMessageNode } from './contracts' 15 | 16 | /** 17 | * Logger exposes the API to print fancy logs to the console. 18 | */ 19 | export class Logger { 20 | /** 21 | * List of actions that can be logged using the logger 22 | */ 23 | public actions: ActionsList = { 24 | success: { 25 | color: 'green', 26 | badge: figures.tick, 27 | logLevel: 'info', 28 | }, 29 | fatal: { 30 | color: 'red', 31 | badge: figures.cross, 32 | logLevel: 'error', 33 | }, 34 | error: { 35 | color: 'red', 36 | badge: figures.cross, 37 | logLevel: 'error', 38 | }, 39 | info: { 40 | color: 'blue', 41 | badge: figures.info, 42 | logLevel: 'info', 43 | }, 44 | complete: { 45 | color: 'cyan', 46 | badge: figures.checkboxOn, 47 | logLevel: 'info', 48 | }, 49 | pending: { 50 | color: 'magenta', 51 | badge: figures.checkboxOff, 52 | logLevel: 'info', 53 | }, 54 | create: { 55 | color: 'green', 56 | badge: figures.tick, 57 | logLevel: 'info', 58 | }, 59 | update: { 60 | color: 'yellow', 61 | badge: figures.tick, 62 | logLevel: 'info', 63 | }, 64 | delete: { 65 | color: 'blue', 66 | badge: figures.tick, 67 | logLevel: 'info', 68 | }, 69 | watch: { 70 | color: 'yellow', 71 | badge: figures.ellipsis, 72 | logLevel: 'info', 73 | }, 74 | start: { 75 | color: 'green', 76 | badge: figures.play, 77 | logLevel: 'info', 78 | }, 79 | stop: { 80 | color: 'magenta', 81 | badge: figures.squareSmallFilled, 82 | logLevel: 'info', 83 | }, 84 | compile: { 85 | color: 'yellow', 86 | badge: figures.pointer, 87 | logLevel: 'info', 88 | }, 89 | skip: { 90 | color: 'magenta', 91 | badge: figures.bullet, 92 | logLevel: 'info', 93 | }, 94 | warn: { 95 | color: 'yellow', 96 | badge: figures.warning, 97 | logLevel: 'info', 98 | }, 99 | } 100 | 101 | /** 102 | * Reference to colors, fake colors are used when `fake` is 103 | * set to true 104 | */ 105 | private colors: Colors | FakeColors 106 | 107 | /** 108 | * Array of logs collected when logger was paused. Helps in 109 | * collecting logs and then filtering them during resume. 110 | */ 111 | private deferredLogs: DeferredMessageNode[] = [] 112 | 113 | /** 114 | * Is logger paused from printing logs 115 | */ 116 | private isPaused: boolean = false 117 | 118 | /** 119 | * Length of the biggest label to keep all log messages 120 | * justified 121 | */ 122 | private biggestLabel: number 123 | 124 | /** 125 | * An array of logs collected only when `fake` is set 126 | * to true 127 | */ 128 | public logs: string[] = [] 129 | 130 | constructor(private baseOptions?: Partial> & { fake?: boolean }) { 131 | this.configure() 132 | this.computeBiggestLabel() 133 | } 134 | 135 | /** 136 | * Configures the logger 137 | */ 138 | private configure() { 139 | this.baseOptions = Object.assign( 140 | { 141 | color: true, 142 | icon: true, 143 | underline: true, 144 | fake: false, 145 | }, 146 | this.baseOptions 147 | ) 148 | 149 | this.colors = this.baseOptions!.fake ? new FakeColors() : new Colors() 150 | } 151 | 152 | /** 153 | * Computes the length of the biggest label including it's icon. Required 154 | * to justify content 155 | */ 156 | private computeBiggestLabel() { 157 | this.biggestLabel = Math.max( 158 | ...Object.keys(this.actions).map((name: keyof ActionsList) => { 159 | const action = this.actions[name] 160 | const badge = this.colors[action.color](action.badge) 161 | const label = this.colors[action.color]().underline(name) 162 | return stringWidth(`${badge} ${label}`) 163 | }) 164 | ) 165 | } 166 | 167 | private serializeError( 168 | error: T 169 | ): T { 170 | return Object.getOwnPropertyNames(error).reduce((result, key) => { 171 | result[key] = error[key] 172 | return result 173 | }, {} as T) 174 | } 175 | 176 | /** 177 | * Returns the base message node 178 | */ 179 | private normalizeMessage(message: string | MessageNode): MessageNode { 180 | /** 181 | * Message itself is an error object, so we add icon, color and underline 182 | * to props to it 183 | */ 184 | if (typeof message !== 'string' && message['stack']) { 185 | const serializedMessage = this.serializeError(message) 186 | serializedMessage['icon'] = this.baseOptions!.icon 187 | serializedMessage['color'] = this.baseOptions!.color 188 | serializedMessage['underline'] = this.baseOptions!.underline 189 | return serializedMessage as MessageNode 190 | } 191 | 192 | /** 193 | * Message is a string, so we use the defaults + the message text 194 | */ 195 | if (typeof message === 'string') { 196 | return Object.assign({}, this.baseOptions, { message }) 197 | } 198 | 199 | /** 200 | * Message is an object, but it's message is an error object. In that 201 | * case, we merge the props of message with the defaults and then 202 | * copy them over the message.message error object. CONFUSED? 203 | */ 204 | if (typeof message.message !== 'string' && message.message['stack']) { 205 | const serializedMessage = this.serializeError(message.message) 206 | const options = Object.assign({}, this.baseOptions, message) 207 | serializedMessage['icon'] = options.icon 208 | serializedMessage['color'] = options.color 209 | serializedMessage['underline'] = options.underline 210 | return serializedMessage as MessageNode 211 | } 212 | 213 | return Object.assign({}, this.baseOptions, message) 214 | } 215 | 216 | /** 217 | * Returns whitespace for a given length 218 | */ 219 | private getWhitespace(length: number): string { 220 | return this.baseOptions!.fake ? ' ' : new Array(length + 1).join(' ') 221 | } 222 | 223 | /** 224 | * Returns the icon for a given action type 225 | */ 226 | private getIcon(name: keyof ActionsList, messageNode: Partial): string { 227 | const action = this.actions[name] 228 | if (this.baseOptions!.fake) { 229 | return '' 230 | } 231 | 232 | if (!messageNode.icon) { 233 | return this.getWhitespace(3) 234 | } 235 | 236 | if (!messageNode.color) { 237 | return `${action.badge}${this.getWhitespace(2)}` 238 | } 239 | 240 | return `${this.colors[action.color](action.badge)}${this.getWhitespace(2)}` 241 | } 242 | 243 | /** 244 | * Returns the label for a given action type 245 | */ 246 | private getLabel(name: keyof ActionsList, messageNode: Partial): string { 247 | const action = this.actions[name] 248 | 249 | if (messageNode.color && messageNode.underline) { 250 | return this.colors.underline()[action.color](name) as string 251 | } 252 | 253 | if (messageNode.color) { 254 | return this.colors[action.color](name) as string 255 | } 256 | 257 | return name 258 | } 259 | 260 | /** 261 | * Returns the prefix for the message 262 | */ 263 | private getPrefix(messageNode: Partial): string { 264 | if (messageNode.prefix) { 265 | return `${this.colors.dim(messageNode.prefix)}${this.getWhitespace(1)}` 266 | } 267 | return '' 268 | } 269 | 270 | /** 271 | * Returns the suffix for the message 272 | */ 273 | private getSuffix(messageNode: Partial): string { 274 | if (messageNode.suffix) { 275 | return `${this.getWhitespace(1)}${this.colors.dim().yellow(messageNode.suffix)}` 276 | } 277 | return '' 278 | } 279 | 280 | /** 281 | * Formats error message 282 | */ 283 | private formatStack(name: keyof ActionsList, message: Error | MessageNode) { 284 | if (name !== 'fatal' || !message['stack']) { 285 | return message.message 286 | } 287 | 288 | const stack = message['stack'].split('\n') 289 | return `${stack.shift()}\n${stack 290 | .map((line) => { 291 | return `${this.colors.dim(line)}` 292 | }) 293 | .join('\n')}` 294 | } 295 | 296 | /** 297 | * Invokes `console[logMethod]`, gives opportunity to overwrite the 298 | * method during extend 299 | */ 300 | protected $log(logMethod: string, message: string, args: any[]) { 301 | console[logMethod](message, ...args) 302 | } 303 | 304 | /** 305 | * Prints message node to the console 306 | */ 307 | protected $printMessage(message: DeferredMessageNode) { 308 | const prefix = this.getPrefix(message) 309 | const icon = this.getIcon(message.action, message) 310 | const label = this.getLabel(message.action, message) 311 | const formattedMessage = this.formatStack(message.action, message) 312 | const suffix = this.getSuffix(message) 313 | 314 | if (this.baseOptions!.fake) { 315 | const log = format(`${prefix}${icon}${label} ${formattedMessage}${suffix}`, ...message.args) 316 | this.logs.push(log) 317 | return log 318 | } 319 | 320 | const method = this.actions[message.action].logLevel === 'error' ? 'error' : 'log' 321 | 322 | /** 323 | * Justification whitespace is required justify the text after the 324 | * icon and label 325 | */ 326 | const justifyWhitespace = this.getWhitespace( 327 | this.biggestLabel - stringWidth(`${icon}${label}`) + 2 328 | ) 329 | this.$log( 330 | method, 331 | `${prefix}${icon}${label}${justifyWhitespace}${formattedMessage}${suffix}`, 332 | message.args 333 | ) 334 | } 335 | 336 | /** 337 | * Log message for a given action 338 | */ 339 | public log( 340 | name: keyof ActionsList, 341 | messageNode: string | Error | MessageNode, 342 | ...args: string[] 343 | ) { 344 | const normalizedMessage = this.normalizeMessage(messageNode) 345 | const message = Object.assign({ action: name, args }, normalizedMessage) 346 | 347 | if (this.isPaused) { 348 | this.deferredLogs.push(message) 349 | return 350 | } 351 | 352 | return this.$printMessage(message) 353 | } 354 | 355 | /** 356 | * Print success message 357 | */ 358 | public success(message: string | MessageNode, ...args: string[]) { 359 | return this.log('success', message, ...args) 360 | } 361 | 362 | /** 363 | * Print error message 364 | */ 365 | public error(message: string | Error | MessageNode, ...args: string[]) { 366 | return this.log('error', message, ...args) 367 | } 368 | 369 | /** 370 | * Print fatal message 371 | */ 372 | public fatal(message: string | Error | MessageNode, ...args: string[]) { 373 | return this.log('fatal', message, ...args) 374 | } 375 | 376 | /** 377 | * Print info message 378 | */ 379 | public info(message: string | MessageNode, ...args: string[]) { 380 | return this.log('info', message, ...args) 381 | } 382 | 383 | /** 384 | * Print complete message 385 | */ 386 | public complete(message: string | MessageNode, ...args: string[]) { 387 | return this.log('complete', message, ...args) 388 | } 389 | 390 | /** 391 | * Print pending message 392 | */ 393 | public pending(message: string | MessageNode, ...args: string[]) { 394 | return this.log('pending', message, ...args) 395 | } 396 | 397 | /** 398 | * Print create message 399 | */ 400 | public create(message: string | MessageNode, ...args: string[]) { 401 | return this.log('create', message, ...args) 402 | } 403 | 404 | /** 405 | * Print update message 406 | */ 407 | public update(message: string | MessageNode, ...args: string[]) { 408 | return this.log('update', message, ...args) 409 | } 410 | 411 | /** 412 | * Print delete message 413 | */ 414 | public delete(message: string | MessageNode, ...args: string[]) { 415 | return this.log('delete', message, ...args) 416 | } 417 | 418 | /** 419 | * Print watch message 420 | */ 421 | public watch(message: string | MessageNode, ...args: string[]) { 422 | return this.log('watch', message, ...args) 423 | } 424 | 425 | /** 426 | * Print start message 427 | */ 428 | public start(message: string | MessageNode, ...args: string[]) { 429 | return this.log('start', message, ...args) 430 | } 431 | 432 | /** 433 | * Print stop message 434 | */ 435 | public stop(message: string | MessageNode, ...args: string[]) { 436 | return this.log('stop', message, ...args) 437 | } 438 | 439 | /** 440 | * Print compile message 441 | */ 442 | public compile(message: string | MessageNode, ...args: string[]) { 443 | return this.log('compile', message, ...args) 444 | } 445 | 446 | /** 447 | * Print skip message 448 | */ 449 | public skip(message: string | MessageNode, ...args: string[]) { 450 | return this.log('skip', message, ...args) 451 | } 452 | 453 | /** 454 | * Print skip message 455 | */ 456 | public warn(message: string | MessageNode, ...args: string[]) { 457 | return this.log('warn', message, ...args) 458 | } 459 | 460 | /** 461 | * Pause the logger and collect logs in memory 462 | */ 463 | public pauseLogger() { 464 | this.isPaused = true 465 | } 466 | 467 | /** 468 | * Resume logger and pass a function to decide whether or not 469 | * to print the log 470 | */ 471 | public resumeLogger(filterFn?: (message: DeferredMessageNode) => boolean) { 472 | this.isPaused = false 473 | this.deferredLogs.forEach((log) => { 474 | if (typeof filterFn !== 'function' || filterFn(log)) { 475 | this.$printMessage(log) 476 | } 477 | }) 478 | this.deferredLogs = [] 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /src/contracts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @poppinss/fancy-logs 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Colors } from '@poppinss/colors/build/src/Base' 11 | 12 | /** 13 | * Shape of the acceptable message node 14 | */ 15 | export type MessageNode = { 16 | prefix?: string 17 | suffix?: string 18 | icon?: boolean 19 | color?: boolean 20 | underline?: boolean 21 | message: string | Error 22 | } 23 | 24 | export type DeferredMessageNode = { action: keyof ActionsList; args: any[] } & MessageNode 25 | 26 | /** 27 | * Loggable actions 28 | */ 29 | export type ActionNames = 30 | | 'success' 31 | | 'fatal' 32 | | 'error' 33 | | 'warn' 34 | | 'info' 35 | | 'complete' 36 | | 'pending' 37 | | 'create' 38 | | 'update' 39 | | 'delete' 40 | | 'watch' 41 | | 'start' 42 | | 'stop' 43 | | 'compile' 44 | | 'skip' 45 | 46 | /** 47 | * Action definition 48 | */ 49 | export type Action = { 50 | color: keyof Colors 51 | badge: string 52 | logLevel: 'info' | 'error' 53 | } 54 | 55 | /** 56 | * Shape of list of actions 57 | */ 58 | export type ActionsList = { [action in ActionNames]: Action } 59 | -------------------------------------------------------------------------------- /test/logger.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @poppinss/fancy-logs 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import test from 'japa' 11 | import { Logger } from '../src/Logger' 12 | 13 | test.group('Logger', () => { 14 | test('log message', (assert) => { 15 | const logger = new Logger({ fake: true }) 16 | assert.equal(logger.success('Hello world'), 'underline(green(success)) Hello world') 17 | }) 18 | 19 | test('log message with additional data', (assert) => { 20 | const logger = new Logger({ fake: true }) 21 | assert.equal(logger.success('Hello %s', 'world'), 'underline(green(success)) Hello world') 22 | }) 23 | 24 | test('log message without color', (assert) => { 25 | const logger = new Logger({ fake: true }) 26 | assert.equal(logger.success({ message: 'Hello world', color: false }), 'success Hello world') 27 | }) 28 | 29 | test('log message without underline', (assert) => { 30 | const logger = new Logger({ fake: true }) 31 | assert.equal( 32 | logger.success({ message: 'Hello world', underline: false }), 33 | 'green(success) Hello world' 34 | ) 35 | }) 36 | 37 | test('log message with suffix', (assert) => { 38 | const logger = new Logger({ fake: true }) 39 | assert.equal( 40 | logger.success({ message: 'Hello world', suffix: 'npm' }), 41 | 'underline(green(success)) Hello world dim(yellow(npm))' 42 | ) 43 | }) 44 | 45 | test('log message with prefix', (assert) => { 46 | const logger = new Logger({ fake: true }) 47 | assert.equal( 48 | logger.success({ message: 'Hello world', prefix: 'bash' }), 49 | 'dim(bash) underline(green(success)) Hello world' 50 | ) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "compilerOptions": { 4 | "pretty": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /typedoc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@adonisjs/mrm-preset/_typedoc.js')({ 2 | exclude: ['**/test/*.ts', 'index.ts'], 3 | readme: 'none', 4 | }) 5 | --------------------------------------------------------------------------------