@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 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
+ });