This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2021 Harminder Virk, contributors
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
+362
View File
@@ -0,0 +1,362 @@
![](./assets/logger.png)
# CLI UI
> Command line UI Kit used by AdonisJS
This repo is a command line UI Kit used by the AdonisJS framework to design its command line interfaces.
The kit is highly opinionated and we will not allow configurable settings in the near future. We want to be consistent with our UI's without worrying about the configuration.
[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url]
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of contents
- [Installation](#installation)
- [Usage](#usage)
- [Logger](#logger)
- [`success(message, prefix?, suffix?)`](#successmessage-prefix-suffix)
- [`error(message, prefix?, suffix?)`](#errormessage-prefix-suffix)
- [`fatal(message, prefix?, suffix?)`](#fatalmessage-prefix-suffix)
- [`warning(message, prefix?, suffix?)`](#warningmessage-prefix-suffix)
- [`info(message, prefix?, suffix?)`](#infomessage-prefix-suffix)
- [`debug(message, prefix?, suffix?)`](#debugmessage-prefix-suffix)
- [`log(message)`](#logmessage)
- [`logError(message)`](#logerrormessage)
- [`logUpdate(message)`](#logupdatemessage)
- [Action](#action)
- [`action.succeeded(message)`](#actionsucceededmessage)
- [`action.skipped(message)`](#action%E2%80%8Cskippedmessage)
- [`action.failed(message, errorMessage)`](#actionfailedmessage-errormessage)
- [Instructions](#instructions)
- [Sticker](#sticker)
- [Tasks](#tasks)
- [Task Renderers](#task-renderers)
- [Running tasks](#running-tasks)
- [Verbose renderer](#verbose-renderer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Installation
Install the package from the npm registry by running following command.
```sh
npm i @poppinss/cliui
# Yarn users
yarn add @poppinss/cliui
```
## Usage
Import the components you want to use from the package.
```ts
import { logger, instructions, sticker, tasks, table } from '@poppinss/cliui'
```
```ts
logger.info('hello world')
const spinner = logger.await('downloading')
await someTimeConsumingTask()
spinner.stop()
```
## Logger
![](./assets/logger.png)
The logger exposes the following methods.
#### `success(message, prefix?, suffix?)`
Log success message. The message is printed to `stdout`.
```ts
logger.success('Account created')
// [ success ] Account created
```
Optional prefix
```ts
logger.success('Account created', 'ap-south-1')
// [ap-south-1] [ success ] Account created
```
Optional suffix
```ts
logger.success('Account created', undefined, 'ap-south-1')
// [ success ] Account created (ap-south-1)
```
> The prefix and suffix are support on all logger methods except `logger.action`
### `error(message, prefix?, suffix?)`
Log an error message. The message is printed to `stderr`.
```ts
logger.error('Unable to write. Disk full')
// Or log error object
logger.error(new Error('Unable to write. Disk full'))
// [ error ] Unable to write. Disk full
```
### `fatal(message, prefix?, suffix?)`
The `logger.error` does not print the error stack. You must use `logger.fatal` to print the error stack.
```ts
logger.fatal(new Error('Unable to write. Disk full'))
```
![](./assets/stack.png)
### `warning(message, prefix?, suffix?)`
Print a warning message. Message is written to `stdout`.
```ts
logger.warning('Running out of disk space')
// [ warn ] Running out of disk space
```
### `info(message, prefix?, suffix?)`
Print an info message. Message is again written to `stdout`.
```ts
logger.info('Your account is has been updated')
// [ info ] Your account is has been updated
```
### `debug(message, prefix?, suffix?)`
Print a debug message. Message is printed to `stdout`.
```ts
logger.debug('Something just happened')
// [ debug ] Something just happened
```
### `log(message)`
Similar to `console.log`, but instead uses the Logger renderer.
> We will talk about renderers later in this document, since they make testing of log message little bit easier.
```ts
logger.log('hello world')
```
### `logError(message)`
Similar to `console.error`, but instead use the Logger renderer.
```ts
log.logError('this is an error message')
```
### `logUpdate(message)`
Log a message that overwrites the previously logged message. The method is helpful for building progress bars or animations.
```ts
logger.logUpdate(`downloading ${i}%`)
// Once completed, persist the message on console
logger.logUpdatePersist()
```
Here is a complete example of showing the downloading progress.
```ts
const sleep = () => new Promise((resolve) => setTimeout(resolve, 50))
async function run() {
for (let i = 0; i <= 100; i = i + 2) {
await sleep()
logger.logUpdate(`downloading ${i}%`)
}
logger.logUpdatePersist()
}
run()
```
![](./assets/logger-update.gif)
## Action
![](./assets/actions.png)
In order to log results of an action/task, we make use of the `action` method.
```ts
const action = logger.action('create')
action.succeeded('config/auth.ts')
```
An action can end in one of the following states.
### `action.succeeded(message)`
Action completed successfully
```ts
const action = logger.action('create')
action.succeeded('config/auth.ts')
```
### `action.skipped(message)`
Skipped action
```ts
const action = logger.action('create')
action.skipped('app/Models/User.ts')
```
### `action.failed(message, errorMessage)`
Action failed, an error message is required to share more context
```ts
const action = logger.action('create')
action.failed('server.ts', 'File already exists')
```
## Instructions
![](./assets/instructions.png)
Instructions are mainly the steps we want someone to perform in order to achieve something. For example:
- Display instructions to start the development
- Or display instructions to bundle the code for production
```ts
import { instructions, logger } from '@poppinss/cliui'
instructions()
.add(`cd ${logger.colors.cyan('hello-world')}`)
.add(`Run ${logger.colors.cyan('node ace serve --watch')} to start the server`)
.render()
```
- Calling the `instructions()` begins a new instructions block
- Next, you can add new lines by using the `.add()` method.
- Finally, call the `render()` method to render it on the console.
## Sticker
![](./assets/sticker.png)
Similar to the **instructions**, but a sticker does not prefix the lines with a pointer `>` arrow. Rest is all same.
It is helpful for displaying a message that needs the most attention. For example:
- Update the CLI version
- Or, the address to access the local server
```ts
import { sticker, logger } from '@poppinss/cliui'
sticker()
.add('Started HTTP server')
.add('')
.add(`Local address: ${logger.colors.cyan('http://localhost:3333')}`)
.add(`Network address: ${logger.colors.cyan('http://localhost:3333')}`)
.render()
```
## Tasks
We make use of tasks when performing multiple actions in respond to a command. For example:
- Create a new AdonisJS app
- Or, Setup packages after installation
The UI for the tasks is designed to only handle tasks running in sequence.
### Task Renderers
Task has two renderers `minimal` and `verbose`. The minimal renderer is the default choice and switch to `verbose` in one of the following situations.
- Command line is not interactive (no tty)
- Or someone has explicitly opted for verbose output.
### Running tasks
Following is a very simple example of creating and running multiple tasks.
```ts
import { tasks } from '@poppinss/cliui'
await tasks()
.add('clone repo', async (logger, task) => {
logger.info(`cloning ${someRepoUrl}`)
await performClone()
await task.complete()
})
.add('install dependencies', async (logger, task) => {
const spinner = logger.await('running npm install')
await performInstall()
spinner.stop()
await task.complete()
})
.run()
```
- The `add` method accepts the **task title** and the callback function to invoke in order to perform the task
- Once, you are done with the task jobs, you must call `await task.complete()` to complete the task. The `await` is important here.
- In order to **mark task as failed**, you can call the `task.fail` method. All upcoming tasks will be stopped in case of a failure.
`ts await task.fail(new Error('Network error'))`
By default, the `minimal` renderer is used and pivots to the verbose renderer only when terminal is not interactive.
![](./assets/tasks-minimal.gif)
### Verbose renderer
In order to run tasks explicitly in the verbose mode, you can create the tasks instance using `tasks.verbose()` method.
```ts
tasks.verbose().add().add().run()
```
![](./assets/tasks-verbose.gif)
[gh-workflow-image]: https://img.shields.io/github/workflow/status/poppinss/cliui/test?style=for-the-badge
[gh-workflow-url]: https://github.com/poppinss/cliui/actions/workflows/test.yml "Github action"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
[npm-image]: https://img.shields.io/npm/v/@poppinss/cliui.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@poppinss/cliui 'npm'
[license-image]: https://img.shields.io/npm/l/@poppinss/cliui?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md 'license'
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/poppinss/cliui?label=Synk%20Vulnerabilities&style=for-the-badge
[synk-url]: https://snyk.io/test/github/poppinss/cliui?targetFile=package.json 'synk'
+33
View File
@@ -0,0 +1,33 @@
import { Table } from './src/Table';
import { Logger } from './src/Logger';
import { TaskManager } from './src/Task/Manager';
import { Instructions } from './src/Instructions';
import { MemoryRenderer } from './src/Renderer/Memory';
import { ConsoleRenderer } from './src/Renderer/Console';
export declare function instantiate(testing: boolean): {
table: () => Table;
tasks: {
(): TaskManager;
/**
* Initiate tasks in verbose mode
*/
verbose(): TaskManager;
};
icons: {
tick: string;
cross: string;
bullet: string;
nodejs: string;
pointer: string;
info: string;
warning: string;
squareSmallFilled: string;
};
logger: Logger;
sticker: () => Instructions;
instructions: () => Instructions;
isInteractive: boolean;
supportsColors: boolean;
consoleRenderer: ConsoleRenderer;
testingRenderer: MemoryRenderer;
};
+108
View File
@@ -0,0 +1,108 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.instantiate = void 0;
const color_support_1 = __importDefault(require("color-support"));
const Icons_1 = require("./src/Icons");
const Table_1 = require("./src/Table");
const Logger_1 = require("./src/Logger");
const Manager_1 = require("./src/Task/Manager");
const Instructions_1 = require("./src/Instructions");
const Memory_1 = require("./src/Renderer/Memory");
const Console_1 = require("./src/Renderer/Console");
function instantiate(testing) {
/**
* Is terminal interactive or not. The code is copied from
* https://github.com/sindresorhus/is-interactive/blob/master/index.js.
*
* Yes, we can install it as a dependency, but decided to copy/paste 4
* lines. NO STRONG REASONS BEHIND IT
*/
const isInteractive = Boolean(process.stdout && process.stdout.isTTY && process.env.TERM !== 'dumb' && !('CI' in process.env));
/**
* Whether or not colors are enabled. They are enabled by default,
* unless the terminal doesn't support color. Also "FORCE_COLOR"
* env variable enables them forcefully.
*/
const supportsColors = !!process.env.FORCE_COLOR || color_support_1.default.level > 0;
/**
* The renderer used in the testing mode. One can access it to listen
* for the log messages. Also, the memory renderer only works when
* the "CLI_UI_IS_TESTING" flag is set
*/
const testingRenderer = new Memory_1.MemoryRenderer();
/**
* Console renderer outputs to the console. We do not export it, since one
* cannot do much by having an access to it.
*/
const consoleRenderer = new Console_1.ConsoleRenderer();
/**
* Logger
*/
const logger = new Logger_1.Logger({ colors: supportsColors, interactive: isInteractive }, testing);
logger.useRenderer(testing ? testingRenderer : consoleRenderer);
/**
* Reference to the instructions block to render a set of lines inside
* a box.
*/
const instructions = () => {
const instructionsInstance = new Instructions_1.Instructions({ colors: supportsColors, icons: true }, testing);
instructionsInstance.useRenderer(testing ? testingRenderer : consoleRenderer);
return instructionsInstance;
};
/**
* Similar to instructions. But the lines are not prefix with a pointer `>`
*/
const sticker = () => {
const stickerInstance = new Instructions_1.Instructions({ colors: supportsColors, icons: false }, testing);
stickerInstance.useRenderer(testing ? testingRenderer : consoleRenderer);
return stickerInstance;
};
/**
* Initiates a group of tasks
*/
const tasks = () => {
const manager = new Manager_1.TaskManager({ colors: supportsColors, interactive: isInteractive }, testing);
manager.useRenderer(testing ? testingRenderer : consoleRenderer);
return manager;
};
/**
* Initiate tasks in verbose mode
*/
tasks.verbose = () => {
const manager = new Manager_1.TaskManager({ colors: supportsColors, interactive: isInteractive, verbose: true }, testing);
manager.useRenderer(testing ? testingRenderer : consoleRenderer);
return manager;
};
/**
* Instantiate a new table
*/
const table = () => {
const tableInstance = new Table_1.Table({ colors: supportsColors }, testing);
tableInstance.useRenderer(testing ? testingRenderer : consoleRenderer);
return tableInstance;
};
return {
table,
tasks,
icons: Icons_1.icons,
logger,
sticker,
instructions,
isInteractive,
supportsColors,
consoleRenderer,
testingRenderer,
};
}
exports.instantiate = instantiate;
+62
View File
@@ -0,0 +1,62 @@
/**
* Is terminal interactive or not. The code is copied from
* https://github.com/sindresorhus/is-interactive/blob/master/index.js.
*
* Yes, we can install it as a dependency, but decided to copy/paste 4
* lines. NO STRONG REASONS BEHIND IT
*/
export declare const isInteractive: boolean;
/**
* Whether or not colors are enabled. They are enabled by default,
* unless the terminal doesn't support color. Also "FORCE_COLOR"
* env variable enables them forcefully.
*/
export declare const supportsColors: boolean;
/**
* The renderer used in the testing mode. One can access it to listen
* for the log messages. Also, the memory renderer only works when
* the "CLI_UI_IS_TESTING" flag is set
*/
export declare const testingRenderer: import("./src/Renderer/Memory").MemoryRenderer;
/**
* Console renderer outputs to the console. We do not export it, since one
* cannot do much by having an access to it.
*/
export declare const consoleRenderer: import("./src/Renderer/Console").ConsoleRenderer;
/**
* Logger
*/
export declare const logger: import("./src/Logger").Logger;
/**
* Icons
*/
export declare const icons: {
tick: string;
cross: string;
bullet: string;
nodejs: string;
pointer: string;
info: string;
warning: string;
squareSmallFilled: string;
};
/**
* Reference to the instructions block to render a set of lines inside
* a box.
*/
export declare const instructions: () => import("./src/Instructions").Instructions;
/**
* Similar to instructions. But the lines are not prefix with a pointer `>`
*/
export declare const sticker: () => import("./src/Instructions").Instructions;
/**
* Initiates a group of tasks
*/
export declare const tasks: {
(): import("./src/Task/Manager").TaskManager;
verbose(): import("./src/Task/Manager").TaskManager;
};
/**
* Instantiate a new table
*/
export declare const table: () => import("./src/Table").Table;
+63
View File
@@ -0,0 +1,63 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.table = exports.tasks = exports.sticker = exports.instructions = exports.icons = exports.logger = exports.consoleRenderer = exports.testingRenderer = exports.supportsColors = exports.isInteractive = void 0;
const api_1 = require("./api");
const ui = (0, api_1.instantiate)(!!process.env.CLI_UI_IS_TESTING);
/**
* Is terminal interactive or not. The code is copied from
* https://github.com/sindresorhus/is-interactive/blob/master/index.js.
*
* Yes, we can install it as a dependency, but decided to copy/paste 4
* lines. NO STRONG REASONS BEHIND IT
*/
exports.isInteractive = ui.isInteractive;
/**
* Whether or not colors are enabled. They are enabled by default,
* unless the terminal doesn't support color. Also "FORCE_COLOR"
* env variable enables them forcefully.
*/
exports.supportsColors = ui.supportsColors;
/**
* The renderer used in the testing mode. One can access it to listen
* for the log messages. Also, the memory renderer only works when
* the "CLI_UI_IS_TESTING" flag is set
*/
exports.testingRenderer = ui.testingRenderer;
/**
* Console renderer outputs to the console. We do not export it, since one
* cannot do much by having an access to it.
*/
exports.consoleRenderer = ui.consoleRenderer;
/**
* Logger
*/
exports.logger = ui.logger;
/**
* Icons
*/
exports.icons = ui.icons;
/**
* Reference to the instructions block to render a set of lines inside
* a box.
*/
exports.instructions = ui.instructions;
/**
* Similar to instructions. But the lines are not prefix with a pointer `>`
*/
exports.sticker = ui.sticker;
/**
* Initiates a group of tasks
*/
exports.tasks = ui.tasks;
/**
* Instantiate a new table
*/
exports.table = ui.table;
+5
View File
@@ -0,0 +1,5 @@
import { Colors as BaseColors } from '@poppinss/colors/build/src/Base';
/**
* Returns the colors instance based upon the environment
*/
export declare function getBest(testing: boolean, enabled: boolean): BaseColors;
+25
View File
@@ -0,0 +1,25 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getBest = void 0;
const colors_1 = require("@poppinss/colors");
/**
* Returns the colors instance based upon the environment
*/
function getBest(testing, enabled) {
if (!enabled) {
return new colors_1.Raw();
}
if (testing) {
return new colors_1.FakeColors();
}
return new colors_1.Colors();
}
exports.getBest = getBest;
+103
View File
@@ -0,0 +1,103 @@
import { Logger } from '../Logger';
/**
* Shape of the renderer contract. Except the spinner, every
* interface accepts a renderer
*/
export interface RendererContract {
log(message: string): void;
logError(message: string): void;
logUpdate(message: string): void;
logUpdateDone(): void;
}
/**
* Task update listener. Mainly used by the task renderers
*/
export declare type UpdateListener = (task: TaskContract) => void;
/**
* Shape of a task
*/
export interface TaskContract {
title: string;
state: 'idle' | 'running' | 'failed' | 'succeeded';
duration?: string;
completionMessage?: string | {
message: string;
stack?: string;
};
start(): this;
onUpdate(callback: UpdateListener): this;
complete(message?: string): this;
fail(error: string | {
message: string;
stack?: string;
}): this;
}
/**
* Callback passed while registering task with the task
* manager
*/
export declare type TaskCallback = (logger: Logger, task: {
fail: (error: string | {
message: string;
stack?: string;
}) => Promise<void>;
complete: (message?: string) => Promise<void>;
}) => void | Promise<void>;
/**
* Options accepted by the tasks renderers
*/
export declare type TaskRendererOptions = {
colors: boolean;
interactive: boolean;
};
/**
* Options accepted by the tasks manager
*/
export declare type TaskManagerOptions = TaskRendererOptions & {
verbose: boolean;
};
/**
* Logging types
*/
export declare type LoggingTypes = 'success' | 'error' | 'fatal' | 'warning' | 'info' | 'debug' | 'await';
/**
* Options accepted by the logger
*/
export declare type LoggerOptions = {
colors: boolean;
labelColors: boolean;
dim: boolean;
dimLabels: boolean;
interactive: boolean;
};
/**
* Options accepted by table
*/
export declare type TableOptions = {
colors: boolean;
};
export declare type TableRow = (string | {
colSpan?: number;
hAlign?: 'left' | 'center' | 'right';
content: string;
} | {
rowSpan?: number;
vAlign?: 'top' | 'center' | 'bottom';
content: string;
})[] | {
[key: string]: string[];
};
/**
* Options accepted by instructions
*/
export declare type InstructionsOptions = {
icons: boolean;
colors: boolean;
};
/**
* Shape of the instructions line
*/
export declare type InstructionsLine = {
text: string;
width: number;
};
+10
View File
@@ -0,0 +1,10 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+10
View File
@@ -0,0 +1,10 @@
export declare const icons: {
tick: string;
cross: string;
bullet: string;
nodejs: string;
pointer: string;
info: string;
warning: string;
squareSmallFilled: string;
};
+33
View File
@@ -0,0 +1,33 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.icons = void 0;
const { platform } = process;
exports.icons = platform === 'win32' && !process.env.WT_SESSION
? {
tick: '√',
cross: '×',
bullet: '*',
nodejs: '♦',
pointer: '>',
info: 'i',
warning: '‼',
squareSmallFilled: '[█]',
}
: {
tick: '✔',
cross: '✖',
bullet: '●',
nodejs: '⬢',
pointer: '',
info: '',
warning: '⚠',
squareSmallFilled: '◼',
};
+100
View File
@@ -0,0 +1,100 @@
import { InstructionsOptions, RendererContract } from '../Contracts';
/**
* The API to render instructions wrapped inside a box
*/
export declare class Instructions {
private testing;
private state;
/**
* Renderer to use for rendering instructions
*/
private renderer?;
/**
* Line of the widest line inside instructions content
*/
private widestLineLength;
/**
* Number of white spaces on the left of the box
*/
private leftPadding;
/**
* Number of white spaces on the right of the box
*/
private rightPadding;
/**
* Number of empty lines at the top
*/
private paddingTop;
/**
* Number of empty lines at the bottom
*/
private paddingBottom;
/**
* Reference to the colors
*/
private colors;
/**
* Options
*/
options: InstructionsOptions;
constructor(options?: Partial<InstructionsOptions>, testing?: boolean);
/**
* Returns the renderer for rendering the messages
*/
private getRenderer;
/**
* Repeats text for given number of times
*/
private repeat;
/**
* Adds dim transformation
*/
private dim;
/**
* Wraps content inside the left and right vertical lines
*/
private wrapInVerticalLines;
/**
* Returns the top line for the box
*/
private getTopLine;
/**
* Returns the bottom line for the box
*/
private getBottomLine;
/**
* Decorates the instruction line by wrapping it inside the box
* lines
*/
private getContentLine;
/**
* Returns the heading line with the border bottom
*/
private getHeading;
/**
* Returns node for a empty line
*/
private getEmptyLineNode;
/**
* Returns instructions lines with the padding
*/
private getLinesWithPadding;
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Define heading for instructions
*/
heading(text: string): this;
/**
* Add new instruction. Each instruction is rendered
* in a new line inside a box
*/
add(text: string): this;
/**
* Render instructions
*/
render(): void;
}
+208
View File
@@ -0,0 +1,208 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Instructions = void 0;
const cli_boxes_1 = __importDefault(require("cli-boxes"));
const string_width_1 = __importDefault(require("string-width"));
const Icons_1 = require("../Icons");
const Colors_1 = require("../Colors");
const Console_1 = require("../Renderer/Console");
/**
* The box styling used by the instructions
*/
const BOX = cli_boxes_1.default.round;
/**
* Default config options
*/
const DEFAULTS = {
icons: true,
colors: true,
};
/**
* The API to render instructions wrapped inside a box
*/
class Instructions {
constructor(options, testing = false) {
this.testing = testing;
this.state = {
content: [],
};
/**
* Line of the widest line inside instructions content
*/
this.widestLineLength = 0;
/**
* Number of white spaces on the left of the box
*/
this.leftPadding = 4;
/**
* Number of white spaces on the right of the box
*/
this.rightPadding = 8;
/**
* Number of empty lines at the top
*/
this.paddingTop = 1;
/**
* Number of empty lines at the bottom
*/
this.paddingBottom = 1;
this.options = { ...DEFAULTS, ...options };
this.colors = (0, Colors_1.getBest)(this.testing, this.options.colors);
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.renderer) {
this.renderer = new Console_1.ConsoleRenderer();
}
return this.renderer;
}
/**
* Repeats text for given number of times
*/
repeat(text, times) {
return new Array(times + 1).join(text);
}
/**
* Adds dim transformation
*/
dim(text) {
return this.colors.dim(text);
}
/**
* Wraps content inside the left and right vertical lines
*/
wrapInVerticalLines(content, leftWhitespace, rightWhitespace) {
return `${this.dim(BOX.left)}${leftWhitespace}${content}${rightWhitespace}${this.dim(BOX.right)}`;
}
/**
* Returns the top line for the box
*/
getTopLine() {
const horizontalLength = this.widestLineLength + this.leftPadding + this.rightPadding;
const horizontalLine = this.repeat(this.dim(BOX.top), horizontalLength);
return `${this.dim(BOX.topLeft)}${horizontalLine}${this.dim(BOX.topRight)}`;
}
/**
* Returns the bottom line for the box
*/
getBottomLine() {
const horizontalLength = this.widestLineLength + this.leftPadding + this.rightPadding;
const horizontalLine = this.repeat(this.dim(BOX.bottom), horizontalLength);
return `${this.dim(BOX.bottomLeft)}${horizontalLine}${this.dim(BOX.bottomRight)}`;
}
/**
* Decorates the instruction line by wrapping it inside the box
* lines
*/
getContentLine(line) {
const rightWhitespace = this.repeat(' ', this.widestLineLength - line.width + this.rightPadding);
const leftWhitespace = this.repeat(' ', this.leftPadding);
return this.wrapInVerticalLines(line.text, leftWhitespace, rightWhitespace);
}
/**
* Returns the heading line with the border bottom
*/
getHeading() {
if (!this.state.heading) {
return;
}
/**
* Creating the header text
*/
const leftWhitespace = this.repeat(' ', this.leftPadding);
const rightWhitespace = this.repeat(' ', this.widestLineLength - this.state.heading.width + this.rightPadding);
const headingContent = this.wrapInVerticalLines(this.state.heading.text, leftWhitespace, rightWhitespace);
/**
* Creating the heading border bottom
*/
const horizontalLength = this.widestLineLength + this.leftPadding + this.rightPadding;
const borderLine = this.repeat(this.dim(cli_boxes_1.default.single.top), horizontalLength);
const border = this.wrapInVerticalLines(borderLine, '', '');
return `${headingContent}\n${border}`;
}
/**
* Returns node for a empty line
*/
getEmptyLineNode() {
return { text: '', width: 0 };
}
/**
* Returns instructions lines with the padding
*/
getLinesWithPadding() {
const top = new Array(this.paddingTop).fill('').map(this.getEmptyLineNode);
const bottom = new Array(this.paddingBottom).fill('').map(this.getEmptyLineNode);
return top.concat(this.state.content).concat(bottom);
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.renderer = renderer;
return this;
}
/**
* Define heading for instructions
*/
heading(text) {
const width = (0, string_width_1.default)(text);
if (width > this.widestLineLength) {
this.widestLineLength = width;
}
this.state.heading = { text, width };
return this;
}
/**
* Add new instruction. Each instruction is rendered
* in a new line inside a box
*/
add(text) {
text = this.options.icons ? `${this.dim(Icons_1.icons.pointer)} ${text}` : `${text}`;
const width = (0, string_width_1.default)(text);
if (width > this.widestLineLength) {
this.widestLineLength = width;
}
this.state.content.push({ text, width });
return this;
}
/**
* Render instructions
*/
render() {
const renderer = this.getRenderer();
/**
* Render content as it is in testing mode
*/
if (this.testing) {
this.state.heading && renderer.log(this.state.heading.text);
this.state.content.forEach(({ text }) => renderer.log(text));
return;
}
const top = this.getTopLine();
const heading = this.getHeading();
const body = this.getLinesWithPadding()
.map((line) => this.getContentLine(line))
.join('\n');
const bottom = this.getBottomLine();
let output = `${top}\n`;
if (heading) {
output = `${output}${heading}\n`;
}
renderer.log(`${output}${body}\n${bottom}`);
}
}
exports.Instructions = Instructions;
+36
View File
@@ -0,0 +1,36 @@
import { Logger } from '../index';
import { RendererContract } from '../../Contracts';
/**
* Exposes the API to print actions in one of the following three states
*
* - failed
* - succeeded
* - skipped
*/
export declare class Action {
private label;
private logger;
constructor(label: string, logger: Logger);
/**
* Returns the label
*/
private getLabel;
private formatMessage;
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Mark action as successful
*/
succeeded(message: string): void;
/**
* Mark action as skipped
*/
skipped(message: string, skipReason?: string): void;
/**
* Mark action as failed
*/
failed(message: string, errorMessage: string): void;
}
+72
View File
@@ -0,0 +1,72 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Action = void 0;
/**
* Exposes the API to print actions in one of the following three states
*
* - failed
* - succeeded
* - skipped
*/
class Action {
constructor(label, logger) {
this.label = label;
this.logger = logger;
}
/**
* Returns the label
*/
getLabel(label, color) {
if (!this.logger.options.colors) {
return `${label.toUpperCase()}:`;
}
return `${this.logger.colors[color](`${label.toUpperCase()}:`)}`;
}
formatMessage(message) {
if (this.logger.options.dim) {
return this.logger.colors.dim(message);
}
return message;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.logger.useRenderer(renderer);
return this;
}
/**
* Mark action as successful
*/
succeeded(message) {
const label = this.getLabel(this.label, 'green');
this.logger.log(this.formatMessage(`${label} ${message}`));
}
/**
* Mark action as skipped
*/
skipped(message, skipReason) {
let logMessage = this.formatMessage(`${this.getLabel('skip', 'cyan')} ${message}`);
if (skipReason) {
logMessage = `${logMessage} ${this.logger.colors.dim(`(${skipReason})`)}`;
}
this.logger.log(logMessage);
}
/**
* Mark action as failed
*/
failed(message, errorMessage) {
let logMessage = this.formatMessage(`${this.getLabel('error', 'red')} ${message}`);
this.logger.logError(`${logMessage} ${this.logger.colors.dim(`(${errorMessage})`)}`);
}
}
exports.Action = Action;
+56
View File
@@ -0,0 +1,56 @@
import { Logger } from '../index';
/**
* The most simplest spinner to log message with a progress indicator
*/
export declare class Spinner {
private text;
private logger;
private testing;
/**
* Frames to loop over
*/
private frames;
/**
* Animation duration
*/
private interval;
/**
* The state of the spinner
*/
private state;
/**
* The current index for the frames
*/
private currentIndex;
/**
* Builds the message to the print from the text
*/
private messageBuilder;
constructor(text: string, logger: Logger, testing?: boolean);
/**
* Increment index. Also, handles the index overflow
*/
private incrementIndex;
/**
* Loop over the message and animate the spinner
*/
private loop;
/**
* Star the spinner
*/
start(): this;
/**
* Define a custom message builder
*/
useMessageBuilder(messageBuilder: {
render: (text: string) => string;
}): this;
/**
* Update spinner
*/
update(text: string, prefix?: string, suffix?: string): this;
/**
* Stop spinner
*/
stop(): void;
}
+131
View File
@@ -0,0 +1,131 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Spinner = void 0;
/**
* The most simplest spinner to log message with a progress indicator
*/
class Spinner {
constructor(text, logger, testing = false) {
this.text = text;
this.logger = logger;
this.testing = testing;
/**
* Frames to loop over
*/
this.frames = ['. ', '.. ', '...', ' ..', ' .', ' '];
/**
* Animation duration
*/
this.interval = 200;
/**
* The state of the spinner
*/
this.state = 'idle';
/**
* The current index for the frames
*/
this.currentIndex = 0;
/**
* Builds the message to the print from the text
*/
this.messageBuilder = {
render(text) {
if (this.prefix) {
text = `[${this.prefix}] ${text}`;
}
if (this.suffix) {
text = `${text} ${this.suffix}`;
}
return text;
},
};
}
/**
* Increment index. Also, handles the index overflow
*/
incrementIndex() {
this.currentIndex = this.frames.length === this.currentIndex + 1 ? 0 : this.currentIndex + 1;
}
/**
* Loop over the message and animate the spinner
*/
loop() {
if (this.state !== 'running') {
return;
}
/**
* Print the message as it is in testing mode or when the TTY is
* not interactive
*/
if (this.testing || !this.logger.options.interactive) {
this.logger.logUpdate(`${this.messageBuilder.render(this.text)} ${this.frames[2]}`);
return;
}
/**
* Otherwise log the current frame and re-run the function
* with some delay
*/
const frame = this.frames[this.currentIndex];
this.logger.logUpdate(`${this.messageBuilder.render(this.text)} ${frame}`);
setTimeout(() => {
this.incrementIndex();
this.loop();
}, this.interval);
}
/**
* Star the spinner
*/
start() {
this.state = 'running';
this.loop();
return this;
}
/**
* Define a custom message builder
*/
useMessageBuilder(messageBuilder) {
this.messageBuilder = messageBuilder;
return this;
}
/**
* Update spinner
*/
update(text, prefix, suffix) {
if (this.state !== 'running') {
return this;
}
this.text = text;
if (prefix !== undefined) {
this.messageBuilder.prefix = prefix;
}
if (suffix !== undefined) {
this.messageBuilder.suffix = suffix;
}
/**
* Print the message as it is in testing mode or when the TTY is
* not interactive
*/
if (this.testing || !this.logger.options.interactive) {
this.logger.logUpdate(`${this.messageBuilder.render(this.text)} ${this.frames[2]}`);
return this;
}
return this;
}
/**
* Stop spinner
*/
stop() {
this.state = 'stopped';
this.currentIndex = 0;
this.logger.logUpdatePersist();
}
}
exports.Spinner = Spinner;
+121
View File
@@ -0,0 +1,121 @@
import { Action } from './Action';
import { getBest } from '../Colors';
import { Spinner } from './Spinner';
import { LoggerOptions, RendererContract } from '../Contracts';
/**
* Logger exposes the API to log messages with consistent styles
* and colors
*/
export declare class Logger {
private testing;
/**
* Logger configuration options
*/
options: LoggerOptions;
/**
* The colors reference
*/
colors: ReturnType<typeof getBest>;
/**
* The label colors reference
*/
private labelColors;
/**
* The renderer to use to output logs
*/
private renderer?;
constructor(options?: Partial<LoggerOptions>, testing?: boolean);
/**
* Colors the logger label
*/
private colorizeLabel;
/**
* Returns the label for a given logging type
*/
private getLabel;
/**
* Appends the suffix to the message
*/
private addSuffix;
/**
* Prepends the prefix to the message. We do not DIM the prefix, since
* gray doesn't have much brightness already
*/
private addPrefix;
/**
* Prepends the prefix to the message
*/
private prefixLabel;
/**
* Decorate message string
*/
private decorateMessage;
/**
* Decorate message string
*/
private formatStack;
/**
* Returns the renderer for rendering the messages
*/
private getRenderer;
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Log message using the renderer. It is similar to `console.log`
* but uses the underlying renderer instead
*/
log(message: string): void;
/**
* Log message by overwriting the existing one
*/
logUpdate(message: string): void;
/**
* Persist the message logged using [[this.logUpdate]]
*/
logUpdatePersist(): void;
/**
* Log error message using the renderer. It is similar to `console.error`
* but uses the underlying renderer instead
*/
logError(message: string): void;
/**
* Log success message
*/
success(message: string, prefix?: string, suffix?: string): void;
/**
* Log error message
*/
error(message: string | {
message: string;
}, prefix?: string, suffix?: string): void;
/**
* Log fatal message
*/
fatal(message: string | {
message: string;
stack?: string;
}, prefix?: string, suffix?: string): void;
/**
* Log warning message
*/
warning(message: string, prefix?: string, suffix?: string): void;
/**
* Log info message
*/
info(message: string, prefix?: string, suffix?: string): void;
/**
* Log debug message
*/
debug(message: string, prefix?: string, suffix?: string): void;
/**
* Log a message with a spinner
*/
await(message: string, prefix?: string, suffix?: string): Spinner;
/**
* Initiates a new action
*/
action(title: string): Action;
}
+247
View File
@@ -0,0 +1,247 @@
"use strict";
/*
* @poppinss/clui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = void 0;
const Action_1 = require("./Action");
const Colors_1 = require("../Colors");
const Spinner_1 = require("./Spinner");
const Console_1 = require("../Renderer/Console");
/**
* Default config options
*/
const DEFAULTS = {
dim: false,
dimLabels: false,
colors: true,
labelColors: true,
interactive: true,
};
/**
* Logger exposes the API to log messages with consistent styles
* and colors
*/
class Logger {
constructor(options, testing = false) {
this.testing = testing;
this.options = { ...DEFAULTS, ...options };
this.colors = (0, Colors_1.getBest)(this.testing, this.options.colors);
this.labelColors = (0, Colors_1.getBest)(this.testing, this.options.labelColors && this.options.colors);
}
/**
* Colors the logger label
*/
colorizeLabel(color, text) {
if (this.options.dim || this.options.dimLabels) {
return `[ ${this.labelColors.dim()[color](text)} ]`;
}
return `[ ${this.labelColors[color](text)} ]`;
}
/**
* Returns the label for a given logging type
*/
getLabel(type) {
switch (type) {
case 'success':
return this.colorizeLabel('green', 'success');
case 'error':
case 'fatal':
return this.colorizeLabel('red', type);
case 'warning':
return this.colorizeLabel('yellow', 'warn');
case 'info':
return this.colorizeLabel('blue', 'info');
case 'debug':
return this.colorizeLabel('cyan', 'debug');
case 'await':
return this.colorizeLabel('cyan', 'wait');
}
}
/**
* Appends the suffix to the message
*/
addSuffix(message, suffix) {
if (!suffix) {
return message;
}
return `${message} ${this.colors.dim().yellow(`(${suffix})`)}`;
}
/**
* Prepends the prefix to the message. We do not DIM the prefix, since
* gray doesn't have much brightness already
*/
addPrefix(message, prefix) {
if (!prefix) {
return message;
}
prefix = prefix.replace(/%time%/, new Date().toISOString());
return `${this.colors.dim(`[${prefix}]`)} ${message}`;
}
/**
* Prepends the prefix to the message
*/
prefixLabel(message, label) {
return `${label} ${message}`;
}
/**
* Decorate message string
*/
decorateMessage(message) {
if (this.options.dim) {
return this.colors.dim(message);
}
return message;
}
/**
* Decorate message string
*/
formatStack(stack) {
if (!stack) {
return '';
}
return `\n${stack
.split('\n')
.splice(1)
.map((line) => {
return `${this.colors.dim(line)}`;
})
.join('\n')}`;
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.renderer) {
this.renderer = new Console_1.ConsoleRenderer();
}
return this.renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.renderer = renderer;
return this;
}
/**
* Log message using the renderer. It is similar to `console.log`
* but uses the underlying renderer instead
*/
log(message) {
this.getRenderer().log(message);
}
/**
* Log message by overwriting the existing one
*/
logUpdate(message) {
this.getRenderer().logUpdate(message);
}
/**
* Persist the message logged using [[this.logUpdate]]
*/
logUpdatePersist() {
this.getRenderer().logUpdateDone();
}
/**
* Log error message using the renderer. It is similar to `console.error`
* but uses the underlying renderer instead
*/
logError(message) {
this.getRenderer().logError(message);
}
/**
* Log success message
*/
success(message, prefix, suffix) {
message = this.decorateMessage(message);
message = this.prefixLabel(message, this.getLabel('success'));
message = this.addPrefix(message, prefix);
message = this.addSuffix(message, suffix);
this.log(message);
}
/**
* Log error message
*/
error(message, prefix, suffix) {
message = typeof message === 'string' ? message : message.message;
message = this.decorateMessage(message);
message = this.prefixLabel(message, this.getLabel('error'));
message = this.addPrefix(message, prefix);
message = this.addSuffix(message, suffix);
this.logError(message);
}
/**
* Log fatal message
*/
fatal(message, prefix, suffix) {
const stack = this.formatStack(typeof message === 'string' ? undefined : message.stack);
message = typeof message === 'string' ? message : message.message;
message = this.decorateMessage(message);
message = this.prefixLabel(message, this.getLabel('error'));
message = this.addPrefix(message, prefix);
message = this.addSuffix(message, suffix);
this.logError(`${message}${stack}`);
}
/**
* Log warning message
*/
warning(message, prefix, suffix) {
message = this.decorateMessage(message);
message = this.prefixLabel(message, this.getLabel('warning'));
message = this.addPrefix(message, prefix);
message = this.addSuffix(message, suffix);
this.log(message);
}
/**
* Log info message
*/
info(message, prefix, suffix) {
message = this.decorateMessage(message);
message = this.prefixLabel(message, this.getLabel('info'));
message = this.addPrefix(message, prefix);
message = this.addSuffix(message, suffix);
this.log(message);
}
/**
* Log debug message
*/
debug(message, prefix, suffix) {
message = this.decorateMessage(message);
message = this.prefixLabel(message, this.getLabel('debug'));
message = this.addPrefix(message, prefix);
message = this.addSuffix(message, suffix);
this.log(message);
}
/**
* Log a message with a spinner
*/
await(message, prefix, suffix) {
const messageBuilder = {
prefix: prefix,
suffix: suffix,
logger: this,
render(text) {
text = this.logger.decorateMessage(text);
text = this.logger.prefixLabel(text, this.logger.getLabel('await'));
text = this.logger.addPrefix(text, this.prefix);
text = this.logger.addSuffix(text, this.suffix);
return text;
},
};
return new Spinner_1.Spinner(message, this, this.testing).useMessageBuilder(messageBuilder).start();
}
/**
* Initiates a new action
*/
action(title) {
return new Action_1.Action(title, this);
}
}
exports.Logger = Logger;
+19
View File
@@ -0,0 +1,19 @@
import { RendererContract } from '../Contracts';
/**
* Renders messages to the "stdout" and "stderr"
*/
export declare class ConsoleRenderer implements RendererContract {
log(message: string): void;
/**
* Log message by overwriting the existing one
*/
logUpdate(message: string): void;
/**
* Persist the last logged message
*/
logUpdateDone(): void;
/**
* Log error
*/
logError(message: string): void;
}
+42
View File
@@ -0,0 +1,42 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConsoleRenderer = void 0;
const log_update_1 = __importDefault(require("log-update"));
/**
* Renders messages to the "stdout" and "stderr"
*/
class ConsoleRenderer {
log(message) {
console.log(message);
}
/**
* Log message by overwriting the existing one
*/
logUpdate(message) {
(0, log_update_1.default)(message);
}
/**
* Persist the last logged message
*/
logUpdateDone() {
log_update_1.default.done();
}
/**
* Log error
*/
logError(message) {
console.error(message);
}
}
exports.ConsoleRenderer = ConsoleRenderer;
+26
View File
@@ -0,0 +1,26 @@
import { RendererContract } from '../Contracts';
/**
* Keeps log messages within memory. Useful for testing
*/
export declare class MemoryRenderer implements RendererContract {
logs: {
message: string;
stream: 'stdout' | 'stderr';
}[];
/**
* Log message
*/
log(message: string): void;
/**
* For memory renderer the logUpdate is similar to log
*/
logUpdate(message: string): void;
/**
* Its a noop
*/
logUpdateDone(): void;
/**
* Log message as error
*/
logError(message: string): void;
}
+42
View File
@@ -0,0 +1,42 @@
"use strict";
/*
* @poppinss/utils
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryRenderer = void 0;
/**
* Keeps log messages within memory. Useful for testing
*/
class MemoryRenderer {
constructor() {
this.logs = [];
}
/**
* Log message
*/
log(message) {
this.logs.push({ message, stream: 'stdout' });
}
/**
* For memory renderer the logUpdate is similar to log
*/
logUpdate(message) {
this.log(message);
}
/**
* Its a noop
*/
logUpdateDone() { }
/**
* Log message as error
*/
logError(message) {
this.logs.push({ message, stream: 'stderr' });
}
}
exports.MemoryRenderer = MemoryRenderer;
+47
View File
@@ -0,0 +1,47 @@
import { getBest } from '../Colors';
import { RendererContract, TableOptions, TableRow } from '../Contracts';
/**
* Exposes the API to represent a table
*/
export declare class Table {
private testing;
private state;
/**
* The renderer to use to output logs
*/
private renderer?;
/**
* Logger configuration options
*/
options: TableOptions;
/**
* The colors reference
*/
colors: ReturnType<typeof getBest>;
constructor(options?: Partial<TableOptions>, testing?: boolean);
/**
* Returns the renderer for rendering the messages
*/
private getRenderer;
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Define table head
*/
head(headColumns: string[]): this;
/**
* Add a new table row
*/
row(row: TableRow): this;
/**
* Define custom column widths
*/
columnWidths(widths: number[]): this;
/**
* Render table
*/
render(): void;
}
+103
View File
@@ -0,0 +1,103 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Table = void 0;
const cli_table3_1 = __importDefault(require("cli-table3"));
const Colors_1 = require("../Colors");
const Console_1 = require("../Renderer/Console");
/**
* Default config options
*/
const DEFAULTS = {
colors: true,
};
/**
* Exposes the API to represent a table
*/
class Table {
constructor(options, testing = false) {
this.testing = testing;
this.state = {
head: [],
rows: [],
};
this.options = { ...DEFAULTS, ...options };
this.colors = (0, Colors_1.getBest)(this.testing, this.options.colors);
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.renderer) {
this.renderer = new Console_1.ConsoleRenderer();
}
return this.renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.renderer = renderer;
return this;
}
/**
* Define table head
*/
head(headColumns) {
this.state.head = headColumns.map((col) => this.colors.cyan(col));
return this;
}
/**
* Add a new table row
*/
row(row) {
this.state.rows.push(row);
return this;
}
/**
* Define custom column widths
*/
columnWidths(widths) {
this.state.colWidths = widths;
return this;
}
/**
* Render table
*/
render() {
if (this.testing) {
this.getRenderer().log(this.state.head.join('|'));
this.state.rows.forEach((row) => {
const content = Array.isArray(row)
? row.map((cell) => {
if (typeof cell === 'string') {
return cell;
}
return cell.content;
})
: Object.keys(row);
this.getRenderer().log(content.join('|'));
});
return;
}
const cliTable = new cli_table3_1.default({
head: this.state.head,
style: { head: [], border: ['dim'] },
...(this.state.colWidths ? { colWidths: this.state.colWidths } : {}),
});
this.state.rows.forEach((row) => cliTable.push(row));
this.getRenderer().log(cliTable.toString());
}
}
exports.Table = Table;
+50
View File
@@ -0,0 +1,50 @@
import { TaskManagerOptions, TaskCallback, RendererContract } from '../Contracts';
/**
* Exposes the API to create a group of tasks and run them in sequence
*/
export declare class TaskManager {
private testing;
/**
* Options
*/
private options;
/**
* The renderer to use for rendering tasks. Automatically decided
*/
private renderer;
/**
* A set of created tasks
*/
private tasks;
/**
* State of the tasks manager
*/
state: 'idle' | 'running' | 'succeeded' | 'failed';
/**
* Reference to the error raised by the task callback (if any)
*/
error?: any;
constructor(options?: Partial<TaskManagerOptions>, testing?: boolean);
/**
* Instantiates the tasks renderer
*/
private instantiateRenderer;
/**
* Run a given task. The underlying code assumes that tasks are
* executed in sequence.
*/
private runTask;
/**
* Register a new task
*/
add(title: string, callback: TaskCallback): this;
/**
* Define a custom logging renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Run tasks
*/
run(): Promise<void>;
}
+135
View File
@@ -0,0 +1,135 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskManager = void 0;
const index_1 = require("./index");
const Verbose_1 = require("./Renderers/Verbose");
const Minimal_1 = require("./Renderers/Minimal");
/**
* Default set of options
*/
const DEFAULTS = {
colors: true,
interactive: true,
verbose: false,
};
/**
* Exposes the API to create a group of tasks and run them in sequence
*/
class TaskManager {
constructor(options, testing = false) {
this.testing = testing;
/**
* A set of created tasks
*/
this.tasks = [];
/**
* State of the tasks manager
*/
this.state = 'idle';
this.options = { ...DEFAULTS, ...options };
this.instantiateRenderer();
}
/**
* Instantiates the tasks renderer
*/
instantiateRenderer() {
const rendererOptions = {
colors: this.options.colors,
interactive: this.options.interactive,
};
/**
* Using verbose render when verbose option is true or terminal is not
* interactive
*/
if (this.options.verbose || this.testing || !this.options.interactive) {
this.renderer = new Verbose_1.VerboseRenderer(rendererOptions, this.testing);
return;
}
/**
* Otheriwse using the minimal renderer
*/
this.renderer = new Minimal_1.MinimalRenderer(rendererOptions, this.testing);
}
/**
* Run a given task. The underlying code assumes that tasks are
* executed in sequence.
*/
async runTask(index) {
const task = this.tasks[index];
if (!task) {
return;
}
/**
* Start the underlying task
*/
task.task.start();
/**
* Method to invoke when callback has been completed
*/
const complete = async (message) => {
if (task.task.state !== 'running') {
return;
}
task.task.complete(message);
await this.runTask(index + 1);
};
/**
* Method to invoke when callback has been failed
*/
const fail = async (message) => {
if (task.task.state !== 'running') {
return;
}
this.error = message;
this.state = 'failed';
task.task.fail(message);
};
/**
* Invoke callback
*/
try {
await task.callback(this.renderer.logger, { complete, fail });
}
catch (error) {
await fail(error);
}
}
/**
* Register a new task
*/
add(title, callback) {
this.tasks.push({ task: new index_1.Task(title), callback });
return this;
}
/**
* Define a custom logging renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.renderer.useRenderer(renderer);
return this;
}
/**
* Run tasks
*/
async run() {
if (this.state !== 'idle') {
return;
}
this.state = 'running';
this.renderer.tasks(this.tasks.map(({ task }) => task)).render();
await this.runTask(0);
if (this.state === 'running') {
this.state = 'succeeded';
}
}
}
exports.TaskManager = TaskManager;
@@ -0,0 +1,72 @@
import { Logger } from '../../Logger';
import { TaskContract, TaskRendererOptions, RendererContract } from '../../Contracts';
/**
* As the name suggests, render tasks in minimal UI for better viewing
* experience.
*/
export declare class MinimalRenderer {
private options;
private testing;
/**
* The renderer to use to output logs
*/
private renderer?;
/**
* List of registered tasks
*/
private registeredTasks;
/**
* Reference to the logger. We will capture logger messages
* and show them next to the task
*/
logger: Logger;
constructor(options: TaskRendererOptions, testing?: boolean);
/**
* Returns the renderer for rendering the messages
*/
private getRenderer;
/**
* Instantiates the logger and defines a custom renderer
* to log messages in context with the currently running
* task
*/
private instantiateLogger;
/**
* Returns the presentation string for an idle task
*/
private presentIdleTask;
/**
* Returns the presentation string for a running task. The log line is
* updated when logger recieves the message.
*/
private presentRunningTask;
/**
* Returns the presentation string for a failed task
*/
private presentFailedTask;
/**
* Returns the presentation string for a succeeded task
*/
private presentSucceededTask;
/**
* Renders a given task
*/
private renderTask;
/**
* Re-renders all tasks by inspecting their current state
*/
private renderTasks;
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Register tasks to render
*/
tasks(tasks: TaskContract[]): this;
/**
* Render all tasks
*/
render(): void;
}
+143
View File
@@ -0,0 +1,143 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MinimalRenderer = void 0;
const Icons_1 = require("../../Icons");
const Logger_1 = require("../../Logger");
const Console_1 = require("../../Renderer/Console");
/**
* As the name suggests, render tasks in minimal UI for better viewing
* experience.
*/
class MinimalRenderer {
constructor(options, testing = false) {
this.options = options;
this.testing = testing;
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.renderer) {
this.renderer = new Console_1.ConsoleRenderer();
}
return this.renderer;
}
/**
* Instantiates the logger and defines a custom renderer
* to log messages in context with the currently running
* task
*/
instantiateLogger() {
/**
* The minimal renderer must always be used when term
* has support for colors and is tty
*/
this.logger = new Logger_1.Logger({ ...this.options, dim: true }, this.testing);
this.logger.useRenderer({
log: (message) => this.renderTasks(message),
logError: (message) => this.renderTasks(message),
logUpdate: (message) => this.renderTasks(message),
logUpdateDone: () => { },
});
}
/**
* Returns the presentation string for an idle task
*/
presentIdleTask(task) {
return `${this.logger.colors.dim(Icons_1.icons.pointer)} ${this.logger.colors.dim(task.title)}`;
}
/**
* Returns the presentation string for a running task. The log line is
* updated when logger recieves the message.
*/
presentRunningTask(task, logLine) {
let message = `${Icons_1.icons.pointer} ${task.title}`;
if (!logLine) {
return message;
}
const lines = logLine.trim().split('\n');
return `${message}\n ${lines[0]}`;
}
/**
* Returns the presentation string for a failed task
*/
presentFailedTask(task) {
const pointer = this.logger.colors.red(Icons_1.icons.pointer);
const duration = this.logger.colors.dim(task.duration);
let message = `${pointer} ${task.title} ${duration}`;
if (!task.completionMessage) {
return message;
}
const errorMessage = typeof task.completionMessage === 'string'
? task.completionMessage
: task.completionMessage.message;
message = `${message}\n ${this.logger.colors.red(errorMessage)}`;
return message;
}
/**
* Returns the presentation string for a succeeded task
*/
presentSucceededTask(task) {
const pointer = this.logger.colors.green(Icons_1.icons.pointer);
const duration = this.logger.colors.dim(task.duration);
let message = `${pointer} ${task.title} ${duration}`;
if (!task.completionMessage) {
return message;
}
message = `${message}\n ${this.logger.colors.dim(task.completionMessage)}`;
return message;
}
/**
* Renders a given task
*/
renderTask(task, logLine) {
switch (task.state) {
case 'idle':
return this.presentIdleTask(task);
case 'running':
return this.presentRunningTask(task, logLine);
case 'succeeded':
return this.presentSucceededTask(task);
case 'failed':
return this.presentFailedTask(task);
}
}
/**
* Re-renders all tasks by inspecting their current state
*/
renderTasks(logLine) {
this.getRenderer().logUpdate(this.registeredTasks.map((task) => this.renderTask(task, logLine)).join('\n'));
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.renderer = renderer;
return this;
}
/**
* Register tasks to render
*/
tasks(tasks) {
this.registeredTasks = tasks;
return this;
}
/**
* Render all tasks
*/
render() {
this.instantiateLogger();
this.registeredTasks.forEach((task) => task.onUpdate(() => this.renderTasks()));
this.renderTasks();
}
}
exports.MinimalRenderer = MinimalRenderer;
@@ -0,0 +1,55 @@
import { Logger } from '../../Logger';
import { TaskContract, TaskRendererOptions, RendererContract } from '../../Contracts';
/**
* Verbose renderer shows a detailed output of the tasks and the
* messages logged by a given task
*/
export declare class VerboseRenderer {
private options;
private testing;
/**
* The renderer to use to output logs
*/
private renderer?;
/**
* List of registered tasks
*/
private registeredTasks;
/**
* Reference to the logger. We will capture logger messages
* and show them next to the task
*/
logger: Logger;
constructor(options: TaskRendererOptions, testing?: boolean);
/**
* Returns the renderer for rendering the messages
*/
private getRenderer;
/**
* Prefixes pipe to a line of text
*/
private prefixPipe;
/**
* Instantiates the logger and defines a custom renderer
* to log messages in context with the currently running
* task
*/
private instantiateLogger;
/**
* Logs message based upon the state of the task
*/
private updateTask;
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer: RendererContract): this;
/**
* Register tasks to render
*/
tasks(tasks: TaskContract[]): this;
/**
* Render all tasks
*/
render(): void;
}
+108
View File
@@ -0,0 +1,108 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VerboseRenderer = void 0;
const Logger_1 = require("../../Logger");
const Console_1 = require("../../Renderer/Console");
/**
* Verbose renderer shows a detailed output of the tasks and the
* messages logged by a given task
*/
class VerboseRenderer {
constructor(options, testing = false) {
this.options = options;
this.testing = testing;
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.renderer) {
this.renderer = new Console_1.ConsoleRenderer();
}
return this.renderer;
}
/**
* Prefixes pipe to a line of text
*/
prefixPipe(text) {
return text
.split('\n')
.map((line) => `${this.logger.colors.dim('│')} ${line}`)
.join('\n');
}
/**
* Instantiates the logger and defines a custom renderer
* to log messages in context with the currently running
* task
*/
instantiateLogger() {
this.logger = new Logger_1.Logger({ ...this.options, dim: true }, this.testing);
this.logger.useRenderer({
log: (message) => this.getRenderer().log(this.prefixPipe(message)),
logError: (message) => this.getRenderer().logError(this.prefixPipe(message)),
logUpdate: (message) => this.getRenderer().logUpdate(this.prefixPipe(message)),
logUpdateDone: () => this.getRenderer().logUpdateDone(),
});
}
/**
* Logs message based upon the state of the task
*/
updateTask(task) {
/**
* Task started running
*/
if (task.state === 'running') {
this.getRenderer().log(`${this.logger.colors.dim('┌')} ${task.title}`);
return;
}
const pipe = this.logger.colors.dim('└');
const duration = this.logger.colors.dim(`(${task.duration})`);
/**
* Task failed
*/
if (task.state === 'failed') {
task.completionMessage && this.logger.fatal(task.completionMessage);
this.getRenderer().logError(`${pipe} ${this.logger.colors.red('failed')} ${duration}`);
return;
}
/**
* Task succeeded
*/
if (task.state === 'succeeded') {
task.completionMessage && this.logger.colors.green(task.completionMessage);
this.getRenderer().log(`${pipe} ${this.logger.colors.green('completed')} ${duration}`);
return;
}
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.renderer = renderer;
return this;
}
/**
* Register tasks to render
*/
tasks(tasks) {
this.registeredTasks = tasks;
return this;
}
/**
* Render all tasks
*/
render() {
this.instantiateLogger();
this.registeredTasks.forEach((task) => task.onUpdate(($task) => this.updateTask($task)));
}
}
exports.VerboseRenderer = VerboseRenderer;
+43
View File
@@ -0,0 +1,43 @@
import { TaskContract, UpdateListener } from '../Contracts';
/**
* Task exposes a very simple API to create tasks with states, along with a
* listener to listen for the task state updates.
*
* The task itself has does not render anything to the console. The task
* renderers does that.
*/
export declare class Task implements TaskContract {
title: string;
private startTime;
private onUpdateListener;
/**
* Duration of the task. Updated after the task is over
*/
duration?: string;
/**
* Message set after completing the task. Can be an error or the
* a success message
*/
completionMessage?: TaskContract['completionMessage'];
/**
* Task current state
*/
state: TaskContract['state'];
constructor(title: string);
/**
* Bind a listener to listen to the state updates of the task
*/
onUpdate(listener: UpdateListener): this;
/**
* Start the task
*/
start(): this;
/**
* Mark task as completed
*/
complete(message?: string): this;
/**
* Mark task as failed
*/
fail(error: TaskContract['completionMessage']): this;
}
+69
View File
@@ -0,0 +1,69 @@
"use strict";
/*
* @poppinss/cliui
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Task = void 0;
const pretty_hrtime_1 = __importDefault(require("pretty-hrtime"));
/**
* Task exposes a very simple API to create tasks with states, along with a
* listener to listen for the task state updates.
*
* The task itself has does not render anything to the console. The task
* renderers does that.
*/
class Task {
constructor(title) {
this.title = title;
this.onUpdateListener = () => { };
/**
* Task current state
*/
this.state = 'idle';
}
/**
* Bind a listener to listen to the state updates of the task
*/
onUpdate(listener) {
this.onUpdateListener = listener;
return this;
}
/**
* Start the task
*/
start() {
this.state = 'running';
this.startTime = process.hrtime();
this.onUpdateListener && this.onUpdateListener(this);
return this;
}
/**
* Mark task as completed
*/
complete(message) {
this.state = 'succeeded';
this.duration = (0, pretty_hrtime_1.default)(process.hrtime(this.startTime));
this.completionMessage = message;
this.onUpdateListener && this.onUpdateListener(this);
return this;
}
/**
* Mark task as failed
*/
fail(error) {
this.state = 'failed';
this.duration = (0, pretty_hrtime_1.default)(process.hrtime(this.startTime));
this.completionMessage = error;
this.onUpdateListener && this.onUpdateListener(this);
return this;
}
}
exports.Task = Task;
+139
View File
@@ -0,0 +1,139 @@
{
"name": "@poppinss/cliui",
"version": "3.0.5",
"description": "Highly opinionated command line UI KIT",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js",
"build/api.d.ts",
"build/api.js"
],
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node -r @adonisjs/require-ts/build/register ./bin/test.ts",
"clean": "del-cli build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"lint": "eslint . --ext=.ts",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json poppinss/cliui"
},
"keywords": [
"cliui",
"colors",
"progress"
],
"author": "virk,poppinss",
"license": "MIT",
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.13",
"@japa/assert": "^1.3.6",
"@japa/run-failed-tests": "^1.1.0",
"@japa/runner": "^2.2.2",
"@japa/spec-reporter": "^1.3.2",
"@types/node": "^18.11.5",
"commitizen": "^4.2.5",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"doctoc": "^2.2.1",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^4.2.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.1",
"mrm": "^4.1.13",
"np": "^7.6.2",
"prettier": "^2.7.1",
"typescript": "^4.8.4"
},
"nyc": {
"exclude": [
"test"
],
"extension": [
".ts"
]
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"dependencies": {
"@poppinss/colors": "^3.0.3",
"cli-boxes": "^3.0.0",
"cli-table3": "^0.6.3",
"color-support": "^1.1.3",
"log-update": "^4.0.0",
"pretty-hrtime": "^1.0.3",
"string-width": "^4.2.2"
},
"directories": {
"example": "example",
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/poppinss/cliui.git"
},
"bugs": {
"url": "https://github.com/poppinss/cliui/issues"
},
"homepage": "https://github.com/poppinss/cliui#readme",
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": true
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
}
}