@portosaur/logger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/package.json +26 -0
- package/src/index.d.ts +118 -0
- package/src/index.mjs +73 -0
- package/src/lib/colors.mjs +65 -0
- package/src/lib/helpers.mjs +153 -0
- package/tests/logger.format.test.mjs +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @portosaur/logger
|
|
2
|
+
|
|
3
|
+
> **The missing Docusaurus-style logger.** A lightweight, Docusaurus-compatible logging library enhanced with semantic levels, flexible alignment, and elegant box rendering.
|
|
4
|
+
|
|
5
|
+
Designed to provide the familiar Docusaurus terminal experience to any CLI project, while adding essential utilities like `consola`-powered boxes and custom `tip`/`note` levels.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @portosaur/logger
|
|
11
|
+
|
|
12
|
+
# or
|
|
13
|
+
npm install @portosaur/logger
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import logger, { colors } from "@portosaur/logger";
|
|
20
|
+
|
|
21
|
+
// Inline log
|
|
22
|
+
logger.info("Starting setup...");
|
|
23
|
+
logger.success("Build complete!");
|
|
24
|
+
logger.warn("Dependency outdated");
|
|
25
|
+
logger.error("Build failed");
|
|
26
|
+
logger.tip("Run dev to preview");
|
|
27
|
+
logger.note("Remember to commit");
|
|
28
|
+
|
|
29
|
+
// Debug — only shown when DEBUG=1 or VERBOSE=1
|
|
30
|
+
logger.debug("Resolved config: /home/user/config.yml");
|
|
31
|
+
|
|
32
|
+
// Box — color pre-applied per level, CI-safe
|
|
33
|
+
logger.success.box("Build complete!", { title: "Result" });
|
|
34
|
+
logger.warn.box("Outdated dependency found", { title: "Warning" });
|
|
35
|
+
logger.note.box("Project initialized", { title: "Next Steps" });
|
|
36
|
+
|
|
37
|
+
// Start — helper that resets view and logs info
|
|
38
|
+
logger.start("Bootstrapping project...");
|
|
39
|
+
|
|
40
|
+
// Semantic colors
|
|
41
|
+
console.log(colors.command("porto dev"));
|
|
42
|
+
console.log(colors.url("https://example.com"));
|
|
43
|
+
console.log(colors.muted("(optional)"));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Log Levels
|
|
47
|
+
|
|
48
|
+
Every level is a `LevelFn`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface LevelFn {
|
|
52
|
+
(msg: string): void;
|
|
53
|
+
box(msg: string, options?: BoxOptions): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface BoxOptions {
|
|
57
|
+
title?: string;
|
|
58
|
+
padding?: number;
|
|
59
|
+
border?: "rounded" | "single" | "double" | "none";
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Level | Label | Notes |
|
|
64
|
+
| :-------- | :---------- | :--------------------------------------- |
|
|
65
|
+
| `info` | `[INFO]` | Standard informational message |
|
|
66
|
+
| `success` | `[SUCCESS]` | Operation completed successfully |
|
|
67
|
+
| `warn` | `[WARNING]` | Non-fatal warning |
|
|
68
|
+
| `error` | `[ERROR]` | Error message |
|
|
69
|
+
| `debug` | `[INFO]` | Only shown when `DEBUG=1` or `VERBOSE=1` |
|
|
70
|
+
| `tip` | `[TIP]` | Magenta colored label |
|
|
71
|
+
| `note` | `[NOTE]` | Gray colored label |
|
|
72
|
+
|
|
73
|
+
## Utilities
|
|
74
|
+
|
|
75
|
+
- **`logger.start(msg)`**: A specialized helper that calls `logger.resetView()` and then `logger.info(msg)`. Ideal for initiating a new process.
|
|
76
|
+
- **`logger.resetView()`**: Clears the console and resets cursor position.
|
|
77
|
+
- **`logger.clear()`**: A standard console clear (no-op in test environments).
|
|
78
|
+
|
|
79
|
+
`.box()` on any level automatically degrades to an inline log when `CI=true`.
|
|
80
|
+
|
|
81
|
+
## Colors
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { colors } from "@portosaur/logger";
|
|
85
|
+
|
|
86
|
+
colors.bold("text");
|
|
87
|
+
colors.command("porto dev"); // cyan bold — shell commands
|
|
88
|
+
colors.url("https://..."); // cyan underline
|
|
89
|
+
colors.muted("(optional)"); // gray — hints
|
|
90
|
+
colors.heading("Result"); // yellow — titles
|
|
91
|
+
colors.label("Name"); // white bold — field labels
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Standard chalk colors (`red`, `green`, `blue`, etc.) and modifiers (`bold`, `dim`, `italic`, etc.) are also available.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
GPL-3.0-only
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@portosaur/logger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Portosaur Logger: CLI output utilities and level normalization.",
|
|
5
|
+
"author": "soymadip",
|
|
6
|
+
"license": "GPL-3.0-only",
|
|
7
|
+
"homepage": "https://soymadip.github.io/portosaur",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/soymadip/portosaur"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./src/index.d.ts",
|
|
16
|
+
"import": "./src/index.mjs",
|
|
17
|
+
"default": "./src/index.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"types": "./src/index.d.ts",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@docusaurus/logger": "^3.10.0",
|
|
23
|
+
"chalk": "^5.6.2",
|
|
24
|
+
"consola": "^3.4.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functional color utility (Chalk-like).
|
|
3
|
+
*/
|
|
4
|
+
export type ColorFn = (str: string | number) => string;
|
|
5
|
+
|
|
6
|
+
export interface Colors {
|
|
7
|
+
// Modifiers
|
|
8
|
+
bold: ColorFn;
|
|
9
|
+
dim: ColorFn;
|
|
10
|
+
italic: ColorFn;
|
|
11
|
+
underline: ColorFn;
|
|
12
|
+
strikethrough: ColorFn;
|
|
13
|
+
inverse: ColorFn;
|
|
14
|
+
reset: ColorFn;
|
|
15
|
+
|
|
16
|
+
// Foreground Colors
|
|
17
|
+
black: ColorFn;
|
|
18
|
+
red: ColorFn;
|
|
19
|
+
green: ColorFn;
|
|
20
|
+
yellow: ColorFn;
|
|
21
|
+
blue: ColorFn;
|
|
22
|
+
magenta: ColorFn;
|
|
23
|
+
cyan: ColorFn;
|
|
24
|
+
white: ColorFn;
|
|
25
|
+
gray: ColorFn;
|
|
26
|
+
grey: ColorFn;
|
|
27
|
+
|
|
28
|
+
// Bright Colors
|
|
29
|
+
blackBright: ColorFn;
|
|
30
|
+
redBright: ColorFn;
|
|
31
|
+
greenBright: ColorFn;
|
|
32
|
+
yellowBright: ColorFn;
|
|
33
|
+
blueBright: ColorFn;
|
|
34
|
+
magentaBright: ColorFn;
|
|
35
|
+
cyanBright: ColorFn;
|
|
36
|
+
whiteBright: ColorFn;
|
|
37
|
+
|
|
38
|
+
// Background Colors
|
|
39
|
+
bgBlack: ColorFn;
|
|
40
|
+
bgRed: ColorFn;
|
|
41
|
+
bgGreen: ColorFn;
|
|
42
|
+
bgYellow: ColorFn;
|
|
43
|
+
bgBlue: ColorFn;
|
|
44
|
+
bgMagenta: ColorFn;
|
|
45
|
+
bgCyan: ColorFn;
|
|
46
|
+
bgWhite: ColorFn;
|
|
47
|
+
|
|
48
|
+
// Semantic Wrappers
|
|
49
|
+
muted: ColorFn;
|
|
50
|
+
success: ColorFn;
|
|
51
|
+
warn: ColorFn;
|
|
52
|
+
error: ColorFn;
|
|
53
|
+
info: ColorFn;
|
|
54
|
+
tip: ColorFn;
|
|
55
|
+
heading: ColorFn;
|
|
56
|
+
command: ColorFn;
|
|
57
|
+
label: ColorFn;
|
|
58
|
+
url: ColorFn;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BoxOptions {
|
|
62
|
+
/** The title of the box. */
|
|
63
|
+
title?: string;
|
|
64
|
+
|
|
65
|
+
/** The padding inside the box. Default is 1. */
|
|
66
|
+
padding?: number;
|
|
67
|
+
|
|
68
|
+
/** The border style. Default is "rounded". */
|
|
69
|
+
border?: "rounded" | "single" | "double" | "none";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LevelFn {
|
|
73
|
+
/** Log a message at this level. */
|
|
74
|
+
(msg: string): void;
|
|
75
|
+
|
|
76
|
+
/** Display a styled box using this level's semantic color. */
|
|
77
|
+
box(msg: string, options?: BoxOptions): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface Logger {
|
|
81
|
+
// Standard levels — routed through @docusaurus/logger
|
|
82
|
+
info: LevelFn;
|
|
83
|
+
success: LevelFn;
|
|
84
|
+
warn: LevelFn;
|
|
85
|
+
error: LevelFn;
|
|
86
|
+
|
|
87
|
+
// Gated — only logs when DEBUG or VERBOSE env var is set
|
|
88
|
+
debug: LevelFn;
|
|
89
|
+
|
|
90
|
+
// Custom levels — console.log with aligned colored label
|
|
91
|
+
tip: LevelFn;
|
|
92
|
+
note: LevelFn;
|
|
93
|
+
|
|
94
|
+
// Utilities
|
|
95
|
+
|
|
96
|
+
/** Logs a blank line. */
|
|
97
|
+
newLine(): void;
|
|
98
|
+
|
|
99
|
+
/** Logs a blank line followed by an info message. */
|
|
100
|
+
start(msg: string): void;
|
|
101
|
+
clear(): void;
|
|
102
|
+
resetView(): void;
|
|
103
|
+
|
|
104
|
+
/** Async wait/sleep for a specified number of milliseconds. */
|
|
105
|
+
wait(ms: number, options?: { ci?: boolean; test?: boolean }): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Portosaur Logger
|
|
110
|
+
* Wraps @docusaurus/logger and consola with additional helpers.
|
|
111
|
+
*/
|
|
112
|
+
export const logger: Logger;
|
|
113
|
+
export default logger;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Semantic color utilities (chalk wrapper).
|
|
117
|
+
*/
|
|
118
|
+
export const colors: Colors;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { colors } from "./lib/colors.mjs";
|
|
2
|
+
import { createLevel, createCustomLevel } from "./lib/helpers.mjs";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Portosaur Logger
|
|
6
|
+
* Wraps @docusaurus/logger and consola with additional helpers.
|
|
7
|
+
*/
|
|
8
|
+
export const logger = {
|
|
9
|
+
// Standard levels — routed through @docusaurus/logger
|
|
10
|
+
info: createLevel("info"),
|
|
11
|
+
success: createLevel("success"),
|
|
12
|
+
warn: createLevel("warn"),
|
|
13
|
+
error: createLevel("error"),
|
|
14
|
+
|
|
15
|
+
// Gated — only logs when DEBUG or VERBOSE env var is set
|
|
16
|
+
debug: createLevel("debug", { gated: true }),
|
|
17
|
+
|
|
18
|
+
// Custom levels — console.log with aligned colored label
|
|
19
|
+
tip: createCustomLevel("tip", colors.tip),
|
|
20
|
+
note: createCustomLevel("note", colors.muted),
|
|
21
|
+
|
|
22
|
+
//--------------- Utilities --------------------------
|
|
23
|
+
|
|
24
|
+
/** Prints a visually empty line (using a zero-width space) to avoid console pruning. */
|
|
25
|
+
newLine: () => {
|
|
26
|
+
console.log("\u200B");
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/** Prints a newline followed by an info message. */
|
|
30
|
+
start: (msg) => {
|
|
31
|
+
logger.newLine();
|
|
32
|
+
logger.info(msg);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/** Clears the terminal screen (skipped in test environment). */
|
|
36
|
+
clear: () => {
|
|
37
|
+
if (process.env.NODE_ENV !== "test") {
|
|
38
|
+
process.stdout.write("\x1bc");
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** Resets the terminal view/scrollback (skipped in test environment). */
|
|
43
|
+
resetView: () => {
|
|
44
|
+
if (process.env.NODE_ENV !== "test") {
|
|
45
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Asynchronous wait (sleep) utility that is environment-aware.
|
|
51
|
+
* @param {number} ms - Milliseconds to wait.
|
|
52
|
+
* @param {Object} [options] - Skip options.
|
|
53
|
+
* @param {boolean} [options.ci=false] - If true, do not skip in CI.
|
|
54
|
+
* @param {boolean} [options.test=false] - If true, do not skip in test.
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
wait: (ms, { ci = false, test = false } = {}) => {
|
|
58
|
+
const isTest = process.env.NODE_ENV === "test";
|
|
59
|
+
const isCI = !!process.env.CI;
|
|
60
|
+
|
|
61
|
+
if ((isTest && !test) || (isCI && !ci)) {
|
|
62
|
+
return Promise.resolve();
|
|
63
|
+
}
|
|
64
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* ======================= Export the logger & Helpers ===============================
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
export default logger;
|
|
73
|
+
export { colors };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portosaur Color Palette
|
|
5
|
+
*
|
|
6
|
+
* A full-featured semantic wrapper over chalk.
|
|
7
|
+
* To swap the underlying library, only this file needs to change.
|
|
8
|
+
*/
|
|
9
|
+
export const colors = {
|
|
10
|
+
// ------- Text Modifiers -------
|
|
11
|
+
// Modifiers
|
|
12
|
+
bold: (str) => chalk.bold(str),
|
|
13
|
+
dim: (str) => chalk.dim(str),
|
|
14
|
+
italic: (str) => chalk.italic(str),
|
|
15
|
+
underline: (str) => chalk.underline(str),
|
|
16
|
+
strikethrough: (str) => chalk.strikethrough(str),
|
|
17
|
+
inverse: (str) => chalk.inverse(str),
|
|
18
|
+
reset: (str) => chalk.reset(str),
|
|
19
|
+
|
|
20
|
+
// Foreground Colors
|
|
21
|
+
black: (str) => chalk.black(str),
|
|
22
|
+
red: (str) => chalk.red(str),
|
|
23
|
+
green: (str) => chalk.green(str),
|
|
24
|
+
yellow: (str) => chalk.yellow(str),
|
|
25
|
+
blue: (str) => chalk.blue(str),
|
|
26
|
+
magenta: (str) => chalk.magenta(str),
|
|
27
|
+
cyan: (str) => chalk.cyan(str),
|
|
28
|
+
white: (str) => chalk.white(str),
|
|
29
|
+
gray: (str) => chalk.gray(str),
|
|
30
|
+
grey: (str) => chalk.grey(str),
|
|
31
|
+
|
|
32
|
+
// Bright Colors
|
|
33
|
+
blackBright: (str) => chalk.blackBright(str),
|
|
34
|
+
redBright: (str) => chalk.redBright(str),
|
|
35
|
+
greenBright: (str) => chalk.greenBright(str),
|
|
36
|
+
yellowBright: (str) => chalk.yellowBright(str),
|
|
37
|
+
blueBright: (str) => chalk.blueBright(str),
|
|
38
|
+
magentaBright: (str) => chalk.magentaBright(str),
|
|
39
|
+
cyanBright: (str) => chalk.cyanBright(str),
|
|
40
|
+
whiteBright: (str) => chalk.whiteBright(str),
|
|
41
|
+
|
|
42
|
+
// Background Colors
|
|
43
|
+
bgBlack: (str) => chalk.bgBlack(str),
|
|
44
|
+
bgRed: (str) => chalk.bgRed(str),
|
|
45
|
+
bgGreen: (str) => chalk.bgGreen(str),
|
|
46
|
+
bgYellow: (str) => chalk.bgYellow(str),
|
|
47
|
+
bgBlue: (str) => chalk.bgBlue(str),
|
|
48
|
+
bgMagenta: (str) => chalk.bgMagenta(str),
|
|
49
|
+
bgCyan: (str) => chalk.bgCyan(str),
|
|
50
|
+
bgWhite: (str) => chalk.bgWhite(str),
|
|
51
|
+
|
|
52
|
+
// Semantic Wrappers
|
|
53
|
+
muted: (str) => chalk.gray(str), // subdued hints, placeholders
|
|
54
|
+
success: (str) => chalk.green(str), // success messages
|
|
55
|
+
warn: (str) => chalk.yellow(str), // warnings, back-nav
|
|
56
|
+
error: (str) => chalk.red(str), // errors
|
|
57
|
+
info: (str) => chalk.blue(str), // informational text
|
|
58
|
+
tip: (str) => chalk.magenta.bold(str), // [TIP] prefix
|
|
59
|
+
heading: (str) => chalk.yellow(str), // note/box headings
|
|
60
|
+
command: (str) => chalk.cyan.bold(str), // shell commands / code
|
|
61
|
+
label: (str) => chalk.white.bold(str), // field labels
|
|
62
|
+
url: (str) => chalk.cyanBright.underline(str), // clickable URLs
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default colors;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import dLogger from "@docusaurus/logger";
|
|
2
|
+
import { consola } from "consola";
|
|
3
|
+
import { colors } from "./colors.mjs";
|
|
4
|
+
|
|
5
|
+
// ================= Message Formatting ====================
|
|
6
|
+
|
|
7
|
+
export const TARGET_WIDTH = 10;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format messages for Docusaurus logger with a fixed label column.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} rawMsg - The message to log.
|
|
13
|
+
* @param {string} levelLabel - The level (info, success, etc).
|
|
14
|
+
* @returns {string} The formatted message.
|
|
15
|
+
*/
|
|
16
|
+
export function _formatMsg(rawMsg, levelLabel) {
|
|
17
|
+
// Parse input message
|
|
18
|
+
const s = rawMsg === undefined || rawMsg === null ? "" : String(rawMsg);
|
|
19
|
+
const leading = s.match(/^\n+/)?.[0] || "";
|
|
20
|
+
const body = s.replace(/^\n+/, "");
|
|
21
|
+
|
|
22
|
+
// Return early if empty
|
|
23
|
+
if (body === "") {
|
|
24
|
+
return leading;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Prepare label and calculate padding
|
|
28
|
+
const LABEL_MAP = {
|
|
29
|
+
warn: "WARNING",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const labelText = `[${(LABEL_MAP[levelLabel] || levelLabel).toUpperCase()}]`;
|
|
33
|
+
const lines = body.split(/\r?\n/);
|
|
34
|
+
|
|
35
|
+
// Format each line with proper indentation
|
|
36
|
+
const formatted = lines.map((ln, idx) => {
|
|
37
|
+
if (idx === 0) {
|
|
38
|
+
const padding = TARGET_WIDTH - (labelText.length + 1);
|
|
39
|
+
return " ".repeat(Math.max(0, padding)) + ln;
|
|
40
|
+
}
|
|
41
|
+
return " ".repeat(TARGET_WIDTH) + ln;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Return formatted output
|
|
45
|
+
return leading + formatted.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ================= Routing & Logging ====================
|
|
49
|
+
|
|
50
|
+
// Levels with a native method in @docusaurus/logger
|
|
51
|
+
const DOCUSAURUS_LEVELS = new Set(["info", "success", "warn", "error"]);
|
|
52
|
+
|
|
53
|
+
export const IS_CI = !!process.env.CI;
|
|
54
|
+
|
|
55
|
+
function dlog(msg, level) {
|
|
56
|
+
const s = msg === undefined || msg === null ? "" : String(msg);
|
|
57
|
+
const leading = s.match(/^\n+/)?.[0] || "";
|
|
58
|
+
const body = s.replace(/^\n+/, "");
|
|
59
|
+
|
|
60
|
+
// Write leading newlines
|
|
61
|
+
if (leading) {
|
|
62
|
+
process.stdout.write(leading);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Format and route the message
|
|
66
|
+
const formatted = _formatMsg(body, level);
|
|
67
|
+
const dl = dLogger.default || dLogger;
|
|
68
|
+
const method = DOCUSAURUS_LEVELS.has(level) ? level : "info";
|
|
69
|
+
return dl[method](formatted);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ================= Box Helper ====================
|
|
73
|
+
|
|
74
|
+
const LEVEL_BOX_COLORS = {
|
|
75
|
+
info: "blue",
|
|
76
|
+
success: "green",
|
|
77
|
+
warn: "yellow",
|
|
78
|
+
error: "red",
|
|
79
|
+
start: "blue",
|
|
80
|
+
debug: "gray",
|
|
81
|
+
tip: "magenta",
|
|
82
|
+
note: "gray",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function makeBox(level, inlineFn) {
|
|
86
|
+
const boxColor = LEVEL_BOX_COLORS[level] ?? "cyan";
|
|
87
|
+
return function box(msg, options = {}) {
|
|
88
|
+
const {
|
|
89
|
+
title = level.toUpperCase(),
|
|
90
|
+
padding = 1,
|
|
91
|
+
border = "rounded",
|
|
92
|
+
} = options;
|
|
93
|
+
|
|
94
|
+
if (IS_CI) {
|
|
95
|
+
// Fallback to plain log — boxes don't render well in raw CI logs
|
|
96
|
+
inlineFn(title ? `${title}: ${msg}` : msg);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
consola.box({
|
|
100
|
+
message: ` ${msg} `,
|
|
101
|
+
title: title ? colors.heading(` ${title} `) : "",
|
|
102
|
+
style: {
|
|
103
|
+
borderColor: boxColor,
|
|
104
|
+
borderStyle: border,
|
|
105
|
+
padding: padding,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ================= Level Factories ====================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Standard level — routed through @docusaurus/logger.
|
|
115
|
+
* @param {string} level
|
|
116
|
+
* @param {{ gated?: boolean }} options - gated: only log when DEBUG/VERBOSE is set
|
|
117
|
+
*/
|
|
118
|
+
export function createLevel(level, { gated = false } = {}) {
|
|
119
|
+
const fn = (msg) => {
|
|
120
|
+
if (gated && !process.env.DEBUG && !process.env.VERBOSE) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
dlog(msg, level);
|
|
124
|
+
};
|
|
125
|
+
fn.box = makeBox(level, fn);
|
|
126
|
+
return fn;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Custom level — uses console.log + _formatMsg with its own colored label.
|
|
131
|
+
* Does NOT route through @docusaurus/logger.
|
|
132
|
+
* @param {string} level
|
|
133
|
+
* @param {Function} colorFn - chalk color function for the label
|
|
134
|
+
*/
|
|
135
|
+
export function createCustomLevel(level, colorFn) {
|
|
136
|
+
const label = `[${level.toUpperCase()}]`;
|
|
137
|
+
const fn = (msg) => {
|
|
138
|
+
// Parse input message
|
|
139
|
+
const s = msg === undefined || msg === null ? "" : String(msg);
|
|
140
|
+
const leading = s.match(/^\n+/)?.[0] || "";
|
|
141
|
+
const body = s.replace(/^\n+/, "");
|
|
142
|
+
|
|
143
|
+
// Write leading newlines
|
|
144
|
+
if (leading) {
|
|
145
|
+
process.stdout.write(leading);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Format and output
|
|
149
|
+
console.log(`${colorFn(label)} ${_formatMsg(body, level)}`);
|
|
150
|
+
};
|
|
151
|
+
fn.box = makeBox(level, fn);
|
|
152
|
+
return fn;
|
|
153
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { _formatMsg, TARGET_WIDTH } from "../src/lib/helpers.mjs";
|
|
3
|
+
|
|
4
|
+
const stripAnsi = (s) => (s || "").replace(/\x1b\[[0-9;]*m/g, "");
|
|
5
|
+
|
|
6
|
+
describe("logger formatting", () => {
|
|
7
|
+
test("single-line message aligns", () => {
|
|
8
|
+
const levelLabel = "success";
|
|
9
|
+
const labelText = `[${levelLabel.toUpperCase()}]`;
|
|
10
|
+
const currentLabelLength = labelText.length + 1;
|
|
11
|
+
const paddingCount = TARGET_WIDTH - currentLabelLength;
|
|
12
|
+
const out = _formatMsg("Installation complete", levelLabel);
|
|
13
|
+
const raw = stripAnsi(out);
|
|
14
|
+
expect(raw).toBe(" ".repeat(paddingCount) + "Installation complete");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("leading newline preserved", () => {
|
|
18
|
+
const levelLabel = "error";
|
|
19
|
+
const labelText = `[${levelLabel.toUpperCase()}]`;
|
|
20
|
+
const currentLabelLength = labelText.length + 1;
|
|
21
|
+
const paddingCount = TARGET_WIDTH - currentLabelLength;
|
|
22
|
+
const out = _formatMsg("\nSetup aborted.", levelLabel);
|
|
23
|
+
const raw = stripAnsi(out);
|
|
24
|
+
expect(raw).toBe("\n" + " ".repeat(paddingCount) + "Setup aborted.");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("multi-line message indents subsequent lines", () => {
|
|
28
|
+
const levelLabel = "info";
|
|
29
|
+
const labelText = `[${levelLabel.toUpperCase()}]`;
|
|
30
|
+
const currentLabelLength = labelText.length + 1;
|
|
31
|
+
const paddingCount = TARGET_WIDTH - currentLabelLength;
|
|
32
|
+
const out = _formatMsg("Line1\nLine2\nLine3", levelLabel);
|
|
33
|
+
const raw = stripAnsi(out);
|
|
34
|
+
const expected =
|
|
35
|
+
" ".repeat(paddingCount) +
|
|
36
|
+
"Line1\n" +
|
|
37
|
+
" ".repeat(TARGET_WIDTH) +
|
|
38
|
+
"Line2\n" +
|
|
39
|
+
" ".repeat(TARGET_WIDTH) +
|
|
40
|
+
"Line3";
|
|
41
|
+
expect(raw).toBe(expected);
|
|
42
|
+
});
|
|
43
|
+
});
|