@heinrichb/console-toolkit 1.0.1
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/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/demo.d.ts +5 -0
- package/dist/demo.js +84 -0
- package/dist/demo.test.d.ts +1 -0
- package/dist/demo.test.js +28 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +234 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +123 -0
- package/dist/progress.d.ts +75 -0
- package/dist/progress.js +43 -0
- package/dist/progress.test.d.ts +1 -0
- package/dist/progress.test.js +101 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brennen Heinrich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/demo.d.ts
ADDED
package/dist/demo.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { printDualColumn, getDragonLines, Printer, interpolateColor, createProgressBar } from "./index";
|
|
2
|
+
import pkg from "../package.json";
|
|
3
|
+
/**
|
|
4
|
+
* Run this demo to visually verify terminal output:
|
|
5
|
+
* bun run src/demo.ts
|
|
6
|
+
*/
|
|
7
|
+
export async function runDemo() {
|
|
8
|
+
console.clear();
|
|
9
|
+
// 1. Test Static Dual Column Print
|
|
10
|
+
console.log("--- Static Dual Column Demo ---");
|
|
11
|
+
const purple = "#A78BFA";
|
|
12
|
+
const blue = "#60A5FA";
|
|
13
|
+
const green = "#34D399";
|
|
14
|
+
const yellow = "#FBBF24";
|
|
15
|
+
const leftContent = [
|
|
16
|
+
{ segments: [{ text: "Package:", style: purple }] },
|
|
17
|
+
{ segments: [{ text: "Version:", style: purple }] },
|
|
18
|
+
{ segments: [{ text: "Status:", style: purple }] }
|
|
19
|
+
];
|
|
20
|
+
const rightContent = [
|
|
21
|
+
{ segments: [{ text: pkg.name, style: blue }] },
|
|
22
|
+
{ segments: [{ text: pkg.version, style: green }] },
|
|
23
|
+
{ segments: [{ text: "Testing live output...", style: yellow }] }
|
|
24
|
+
];
|
|
25
|
+
printDualColumn(leftContent, rightContent, { separator: " => " });
|
|
26
|
+
console.log("\n");
|
|
27
|
+
// 2. Test the Dragon Gradient Preset
|
|
28
|
+
console.log("--- Dragon Gradient Preset ---");
|
|
29
|
+
const dragon = getDragonLines("#EF4444", "#FDE047");
|
|
30
|
+
const printer = new Printer();
|
|
31
|
+
printer.print(dragon);
|
|
32
|
+
console.log("\n");
|
|
33
|
+
// 3. Test Progress Bar
|
|
34
|
+
console.log("--- Interactive Progress Bar Demo ---");
|
|
35
|
+
const interactivePrinter = new Printer({ interactive: true });
|
|
36
|
+
const gray = "#4B5563";
|
|
37
|
+
for (let i = 0; i <= 100; i += 2) {
|
|
38
|
+
const progressColor = interpolateColor("#3B82F6", "#10B981", i / 100);
|
|
39
|
+
const progressLine = createProgressBar({
|
|
40
|
+
progress: i / 100,
|
|
41
|
+
width: 30,
|
|
42
|
+
startChar: "[",
|
|
43
|
+
endChar: "]",
|
|
44
|
+
startStyle: progressColor,
|
|
45
|
+
endStyle: progressColor,
|
|
46
|
+
fillStyle: progressColor,
|
|
47
|
+
emptyStyle: gray,
|
|
48
|
+
percentageStyle: progressColor
|
|
49
|
+
});
|
|
50
|
+
interactivePrinter.print([progressLine]);
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
52
|
+
}
|
|
53
|
+
console.log("\n");
|
|
54
|
+
// 4. Style Codes Demo
|
|
55
|
+
console.log("--- Style Codes Demo ---");
|
|
56
|
+
const styles = [
|
|
57
|
+
{ name: "Default", style: "default" },
|
|
58
|
+
{ name: "Bold", style: "bold" },
|
|
59
|
+
{ name: "Dim", style: "dim" },
|
|
60
|
+
{ name: "Italic", style: "italic" },
|
|
61
|
+
{ name: "Underline", style: "underline" },
|
|
62
|
+
{ name: "Strikethrough", style: "strikethrough" },
|
|
63
|
+
{ name: "Inverse", style: "inverse" },
|
|
64
|
+
{ name: "Hidden", style: "hidden" },
|
|
65
|
+
{ name: "Black", style: "black" },
|
|
66
|
+
{ name: "Red", style: "red" },
|
|
67
|
+
{ name: "Green", style: "green" },
|
|
68
|
+
{ name: "Yellow", style: "yellow" },
|
|
69
|
+
{ name: "Blue", style: "blue" },
|
|
70
|
+
{ name: "Magenta", style: "magenta" },
|
|
71
|
+
{ name: "Cyan", style: "cyan" },
|
|
72
|
+
{ name: "White", style: "white" },
|
|
73
|
+
{ name: "Gray", style: "gray" }
|
|
74
|
+
];
|
|
75
|
+
const styleLines = styles.map((s) => ({
|
|
76
|
+
segments: [
|
|
77
|
+
{ text: s.name.padEnd(15), style: "default" },
|
|
78
|
+
{ text: "Sample Text", style: s.style }
|
|
79
|
+
]
|
|
80
|
+
}));
|
|
81
|
+
const stylePrinter = new Printer();
|
|
82
|
+
stylePrinter.print(styleLines);
|
|
83
|
+
console.log("\n✨ Demo Complete!");
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, test, describe, spyOn, afterEach, beforeEach } from "bun:test";
|
|
2
|
+
import { runDemo } from "./demo";
|
|
3
|
+
describe("Demo Script", () => {
|
|
4
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => undefined);
|
|
5
|
+
const clearSpy = spyOn(console, "clear").mockImplementation(() => undefined);
|
|
6
|
+
const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
7
|
+
const originalTimeout = global.setTimeout;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Mocking setTimeout requires unknown casting due to return type mismatch in Bun (void vs Timer)
|
|
10
|
+
global.setTimeout = ((fn) => {
|
|
11
|
+
fn();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
logSpy.mockClear();
|
|
16
|
+
clearSpy.mockClear();
|
|
17
|
+
stdoutSpy.mockClear();
|
|
18
|
+
global.setTimeout = originalTimeout;
|
|
19
|
+
});
|
|
20
|
+
test("runDemo executes correctly", async () => {
|
|
21
|
+
// Just await the function call; we expect it not to throw.
|
|
22
|
+
await runDemo();
|
|
23
|
+
expect(clearSpy).toHaveBeenCalled();
|
|
24
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Static Dual Column Demo"));
|
|
25
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Demo Complete!"));
|
|
26
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
27
|
+
});
|
|
28
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for styled console output.
|
|
3
|
+
*/
|
|
4
|
+
export type StandardColor = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey";
|
|
5
|
+
export type StyleModifier = "bold" | "dim" | "italic" | "underline" | "reset" | "default" | "hidden" | "inverse" | "strikethrough";
|
|
6
|
+
export type HexColor = `#${string}`;
|
|
7
|
+
/**
|
|
8
|
+
* A style can be a standard color name, a hex color string, or a style modifier.
|
|
9
|
+
* It can also be a raw ANSI string (though discouraged) for backward compatibility or special cases.
|
|
10
|
+
*/
|
|
11
|
+
export type Style = StandardColor | StyleModifier | HexColor | string;
|
|
12
|
+
export interface StyledSegment {
|
|
13
|
+
text: string;
|
|
14
|
+
/** Style or array of styles to apply to the text */
|
|
15
|
+
style: Style | Style[];
|
|
16
|
+
}
|
|
17
|
+
export interface StyledLine {
|
|
18
|
+
segments: StyledSegment[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for the Printer engine.
|
|
22
|
+
*/
|
|
23
|
+
export interface PrinterOptions {
|
|
24
|
+
/** If true, the printer will overwrite previous lines instead of appending new ones. */
|
|
25
|
+
interactive?: boolean;
|
|
26
|
+
/** The default style to apply to padding or separators. */
|
|
27
|
+
defaultStyle?: Style | Style[];
|
|
28
|
+
}
|
|
29
|
+
export declare const RESET = "\u001B[0m";
|
|
30
|
+
/**
|
|
31
|
+
* Converts a hex color string to an RGB object.
|
|
32
|
+
* @param hex - Hex color in the form "#RRGGBB".
|
|
33
|
+
*/
|
|
34
|
+
export declare function hexToRgb(hex: string): {
|
|
35
|
+
r: number;
|
|
36
|
+
g: number;
|
|
37
|
+
b: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Converts RGB to a 24-bit ANSI foreground color escape sequence.
|
|
41
|
+
*/
|
|
42
|
+
export declare function rgbToAnsi(r: number, g: number, b: number): string;
|
|
43
|
+
/**
|
|
44
|
+
* Converts a hex color string directly to an ANSI escape sequence.
|
|
45
|
+
*/
|
|
46
|
+
export declare function hexToAnsi(hex: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
|
|
49
|
+
*/
|
|
50
|
+
export declare function interpolateColor(color1: string, color2: string, factor: number): HexColor;
|
|
51
|
+
/**
|
|
52
|
+
* Resolves a single Style or array of Styles into an ANSI escape sequence.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveStyle(style: Style | Style[]): string;
|
|
55
|
+
/**
|
|
56
|
+
* Gets the plain text length of a StyledLine.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getLineLength(line: StyledLine): number;
|
|
59
|
+
/**
|
|
60
|
+
* Computes the maximum width among an array of StyledLines.
|
|
61
|
+
*/
|
|
62
|
+
export declare function computeMaxWidth(lines: StyledLine[]): number;
|
|
63
|
+
/**
|
|
64
|
+
* Pads a StyledLine to a target width by adding an empty segment at the end.
|
|
65
|
+
*/
|
|
66
|
+
export declare function padLine(line: StyledLine, targetWidth: number, padStyle: Style | Style[]): StyledLine;
|
|
67
|
+
/**
|
|
68
|
+
* Handles rendering StyledLines to the terminal with support for interactive overwriting.
|
|
69
|
+
*/
|
|
70
|
+
export declare class Printer {
|
|
71
|
+
private linesRendered;
|
|
72
|
+
private isInteractive;
|
|
73
|
+
constructor(options?: PrinterOptions);
|
|
74
|
+
/**
|
|
75
|
+
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
76
|
+
*/
|
|
77
|
+
private getClearSequence;
|
|
78
|
+
/**
|
|
79
|
+
* Renders an array of StyledLines to the standard output.
|
|
80
|
+
*/
|
|
81
|
+
print(lines: StyledLine[]): void;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Merges two columns of StyledLines into a single layout.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mergeColumns(leftColumn: StyledLine[], rightColumn: StyledLine[], leftWidth: number, separator: string, defaultStyle: Style | Style[]): StyledLine[];
|
|
87
|
+
/**
|
|
88
|
+
* Prints two columns of styled content to the console.
|
|
89
|
+
*/
|
|
90
|
+
export declare function printDualColumn(left: StyledLine[], right: StyledLine[], options?: {
|
|
91
|
+
leftWidth?: number;
|
|
92
|
+
separator?: string;
|
|
93
|
+
printer?: Printer;
|
|
94
|
+
}): void;
|
|
95
|
+
/**
|
|
96
|
+
* Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
|
|
97
|
+
*/
|
|
98
|
+
export declare function getDragonLines(startColor?: string, endColor?: string): StyledLine[];
|
|
99
|
+
export * from "./progress";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for styled console output.
|
|
3
|
+
*/
|
|
4
|
+
// -----------------
|
|
5
|
+
// Color Utilities
|
|
6
|
+
// -----------------
|
|
7
|
+
const ESC = "\x1b";
|
|
8
|
+
export const RESET = `${ESC}[0m`;
|
|
9
|
+
/**
|
|
10
|
+
* Converts a hex color string to an RGB object.
|
|
11
|
+
* @param hex - Hex color in the form "#RRGGBB".
|
|
12
|
+
*/
|
|
13
|
+
export function hexToRgb(hex) {
|
|
14
|
+
const h = hex.replace(/^#/, "");
|
|
15
|
+
if (h.length !== 6)
|
|
16
|
+
throw new Error("Invalid hex color.");
|
|
17
|
+
return {
|
|
18
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
19
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
20
|
+
b: parseInt(h.substring(4, 6), 16)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts RGB to a 24-bit ANSI foreground color escape sequence.
|
|
25
|
+
*/
|
|
26
|
+
export function rgbToAnsi(r, g, b) {
|
|
27
|
+
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts a hex color string directly to an ANSI escape sequence.
|
|
31
|
+
*/
|
|
32
|
+
export function hexToAnsi(hex) {
|
|
33
|
+
const { r, g, b } = hexToRgb(hex);
|
|
34
|
+
return rgbToAnsi(r, g, b);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Converts a single RGB component to a 2-digit hex string.
|
|
38
|
+
*/
|
|
39
|
+
function toHex(c) {
|
|
40
|
+
const hex = Math.max(0, Math.min(255, Math.round(c))).toString(16);
|
|
41
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
|
|
45
|
+
*/
|
|
46
|
+
export function interpolateColor(color1, color2, factor) {
|
|
47
|
+
const f = Math.max(0, Math.min(1, factor));
|
|
48
|
+
const c1 = hexToRgb(color1);
|
|
49
|
+
const c2 = hexToRgb(color2);
|
|
50
|
+
const r = c1.r + f * (c2.r - c1.r);
|
|
51
|
+
const g = c1.g + f * (c2.g - c1.g);
|
|
52
|
+
const b = c1.b + f * (c2.b - c1.b);
|
|
53
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
54
|
+
}
|
|
55
|
+
// -----------------
|
|
56
|
+
// Style Resolution
|
|
57
|
+
// -----------------
|
|
58
|
+
const STYLE_CODES = {
|
|
59
|
+
reset: "0",
|
|
60
|
+
default: "0",
|
|
61
|
+
bold: "1",
|
|
62
|
+
dim: "2",
|
|
63
|
+
italic: "3",
|
|
64
|
+
underline: "4",
|
|
65
|
+
inverse: "7",
|
|
66
|
+
hidden: "8",
|
|
67
|
+
strikethrough: "9",
|
|
68
|
+
black: "30",
|
|
69
|
+
red: "31",
|
|
70
|
+
green: "32",
|
|
71
|
+
yellow: "33",
|
|
72
|
+
blue: "34",
|
|
73
|
+
magenta: "35",
|
|
74
|
+
cyan: "36",
|
|
75
|
+
white: "37",
|
|
76
|
+
gray: "90",
|
|
77
|
+
grey: "90"
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Resolves a single Style or array of Styles into an ANSI escape sequence.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveStyle(style) {
|
|
83
|
+
if (Array.isArray(style)) {
|
|
84
|
+
return style.map(resolveStyle).join("");
|
|
85
|
+
}
|
|
86
|
+
if (typeof style !== "string") {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
// Check for hex color
|
|
90
|
+
if (style.startsWith("#")) {
|
|
91
|
+
try {
|
|
92
|
+
return hexToAnsi(style);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return ""; // Invalid hex, ignore
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Check for standard styles/colors
|
|
99
|
+
const code = STYLE_CODES[style.toLowerCase()];
|
|
100
|
+
if (code) {
|
|
101
|
+
return `${ESC}[${code}m`;
|
|
102
|
+
}
|
|
103
|
+
// Fallback: return as raw string if it looks like ANSI or just text
|
|
104
|
+
// Ideally we would validate ANSI here, but for flexibility we return it.
|
|
105
|
+
return style;
|
|
106
|
+
}
|
|
107
|
+
// -----------------
|
|
108
|
+
// Line Manipulation Helpers
|
|
109
|
+
// -----------------
|
|
110
|
+
/**
|
|
111
|
+
* Gets the plain text length of a StyledLine.
|
|
112
|
+
*/
|
|
113
|
+
export function getLineLength(line) {
|
|
114
|
+
return line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Computes the maximum width among an array of StyledLines.
|
|
118
|
+
*/
|
|
119
|
+
export function computeMaxWidth(lines) {
|
|
120
|
+
return lines.length > 0 ? Math.max(...lines.map(getLineLength)) : 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Pads a StyledLine to a target width by adding an empty segment at the end.
|
|
124
|
+
*/
|
|
125
|
+
export function padLine(line, targetWidth, padStyle) {
|
|
126
|
+
const currentLength = getLineLength(line);
|
|
127
|
+
if (currentLength < targetWidth) {
|
|
128
|
+
return {
|
|
129
|
+
segments: [...line.segments, { text: " ".repeat(targetWidth - currentLength), style: padStyle }]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return line;
|
|
133
|
+
}
|
|
134
|
+
// -----------------
|
|
135
|
+
// Printer Class
|
|
136
|
+
// -----------------
|
|
137
|
+
/**
|
|
138
|
+
* Handles rendering StyledLines to the terminal with support for interactive overwriting.
|
|
139
|
+
*/
|
|
140
|
+
export class Printer {
|
|
141
|
+
linesRendered = 0;
|
|
142
|
+
isInteractive;
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
this.isInteractive = options.interactive ?? false;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
148
|
+
*/
|
|
149
|
+
getClearSequence() {
|
|
150
|
+
if (!this.isInteractive || this.linesRendered === 0)
|
|
151
|
+
return "";
|
|
152
|
+
return `${ESC}[1A${ESC}[2K\r`.repeat(this.linesRendered);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Renders an array of StyledLines to the standard output.
|
|
156
|
+
*/
|
|
157
|
+
print(lines) {
|
|
158
|
+
let output = this.getClearSequence();
|
|
159
|
+
lines.forEach((line) => {
|
|
160
|
+
line.segments.forEach((seg) => {
|
|
161
|
+
const ansiStyle = resolveStyle(seg.style);
|
|
162
|
+
output += `${ansiStyle}${seg.text}${RESET}`;
|
|
163
|
+
});
|
|
164
|
+
output += "\n";
|
|
165
|
+
});
|
|
166
|
+
process.stdout.write(output);
|
|
167
|
+
this.linesRendered = lines.length;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// -----------------
|
|
171
|
+
// Core Layout & Printing
|
|
172
|
+
// -----------------
|
|
173
|
+
const defaultPrinter = new Printer();
|
|
174
|
+
/**
|
|
175
|
+
* Merges two columns of StyledLines into a single layout.
|
|
176
|
+
*/
|
|
177
|
+
export function mergeColumns(leftColumn, rightColumn, leftWidth, separator, defaultStyle) {
|
|
178
|
+
const maxLines = Math.max(leftColumn.length, rightColumn.length);
|
|
179
|
+
const output = [];
|
|
180
|
+
for (let i = 0; i < maxLines; i++) {
|
|
181
|
+
const left = padLine(leftColumn[i] || { segments: [] }, leftWidth, defaultStyle);
|
|
182
|
+
const right = rightColumn[i] || { segments: [] };
|
|
183
|
+
output.push({
|
|
184
|
+
segments: [...left.segments, { text: separator, style: defaultStyle }, ...right.segments]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return output;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Prints two columns of styled content to the console.
|
|
191
|
+
*/
|
|
192
|
+
export function printDualColumn(left, right, options = {}) {
|
|
193
|
+
const { leftWidth, separator = " ", printer = defaultPrinter } = options;
|
|
194
|
+
const defaultStyle = RESET;
|
|
195
|
+
const finalLeftWidth = leftWidth ?? computeMaxWidth(left);
|
|
196
|
+
const mergedLines = mergeColumns(left, right, finalLeftWidth, separator, defaultStyle);
|
|
197
|
+
printer.print(mergedLines);
|
|
198
|
+
}
|
|
199
|
+
// -----------------
|
|
200
|
+
// Presets
|
|
201
|
+
// -----------------
|
|
202
|
+
/**
|
|
203
|
+
* Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
|
|
204
|
+
*/
|
|
205
|
+
export function getDragonLines(startColor = "#EF4444", endColor = "#F59E0B") {
|
|
206
|
+
const rawDragon = [
|
|
207
|
+
" ^ ^",
|
|
208
|
+
" / \\ //\\",
|
|
209
|
+
" |\\___/| / \\// .\\",
|
|
210
|
+
" /O O \\__ / // | \\ \\",
|
|
211
|
+
"/ / \\_/_/ // | \\ \\",
|
|
212
|
+
"@___@' \\_// // | \\ \\ ",
|
|
213
|
+
" | \\_// // | \\ \\ ",
|
|
214
|
+
" | \\/// | \\ \\ ",
|
|
215
|
+
" _|_ / ) // | \\ _\\",
|
|
216
|
+
" '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.",
|
|
217
|
+
" ,-{ _ `-.|.-~-. .~ `.",
|
|
218
|
+
" '/\\ / ~-. _ .-~ .-~^-. \\",
|
|
219
|
+
" `. { } / \\ \\",
|
|
220
|
+
" .----~-\\. \\-' .~ \\ `. \\^-.",
|
|
221
|
+
" ///.----..> c \\ _ -~ `. ^-` ^-_",
|
|
222
|
+
" ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~",
|
|
223
|
+
" /.-'"
|
|
224
|
+
];
|
|
225
|
+
return rawDragon.map((text, i) => {
|
|
226
|
+
const factor = rawDragon.length <= 1 ? 0 : i / (rawDragon.length - 1);
|
|
227
|
+
const colorStyle = interpolateColor(startColor, endColor, factor);
|
|
228
|
+
return { segments: [{ text, style: colorStyle }] };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
// -----------------
|
|
232
|
+
// Progress Bar
|
|
233
|
+
// -----------------
|
|
234
|
+
export * from "./progress";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { expect, test, describe, spyOn, afterEach } from "bun:test";
|
|
2
|
+
import { hexToRgb, rgbToAnsi, hexToAnsi, interpolateColor, getLineLength, computeMaxWidth, padLine, Printer, printDualColumn, getDragonLines, mergeColumns, resolveStyle } from "./index";
|
|
3
|
+
const lineA = { segments: [{ text: "Hello", style: [] }] };
|
|
4
|
+
const lineB = { segments: [{ text: "World!!", style: [] }] };
|
|
5
|
+
describe("Color Utilities", () => {
|
|
6
|
+
test("hexToRgb converts correctly", () => {
|
|
7
|
+
expect(hexToRgb("#FFFFFF")).toEqual({ r: 255, g: 255, b: 255 });
|
|
8
|
+
expect(hexToRgb("#000000")).toEqual({ r: 0, g: 0, b: 0 });
|
|
9
|
+
});
|
|
10
|
+
test("hexToRgb throws on invalid hex", () => {
|
|
11
|
+
expect(() => hexToRgb("invalid")).toThrow("Invalid hex color.");
|
|
12
|
+
expect(() => hexToRgb("#FFF")).toThrow("Invalid hex color.");
|
|
13
|
+
});
|
|
14
|
+
test("rgbToAnsi converts correctly", () => {
|
|
15
|
+
expect(rgbToAnsi(255, 255, 255)).toBe("\x1b[38;2;255;255;255m");
|
|
16
|
+
expect(rgbToAnsi(0, 0, 0)).toBe("\x1b[38;2;0;0;0m");
|
|
17
|
+
});
|
|
18
|
+
test("hexToAnsi converts hex string directly to ANSI", () => {
|
|
19
|
+
expect(hexToAnsi("#FFFFFF")).toBe("\x1b[38;2;255;255;255m");
|
|
20
|
+
expect(hexToAnsi("#000000")).toBe("\x1b[38;2;0;0;0m");
|
|
21
|
+
expect(hexToAnsi("#FF0000")).toBe("\x1b[38;2;255;0;0m");
|
|
22
|
+
});
|
|
23
|
+
test("interpolateColor returns hex string", () => {
|
|
24
|
+
const start = "#000000";
|
|
25
|
+
const end = "#ffffff";
|
|
26
|
+
expect(interpolateColor(start, end, 0.5)).toBe("#808080");
|
|
27
|
+
});
|
|
28
|
+
test("interpolateColor clamps factors", () => {
|
|
29
|
+
expect(interpolateColor("#000000", "#ffffff", -1)).toBe("#000000");
|
|
30
|
+
expect(interpolateColor("#000000", "#ffffff", 2)).toBe("#ffffff");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("Style Resolution", () => {
|
|
34
|
+
test("resolveStyle handles standard colors", () => {
|
|
35
|
+
expect(resolveStyle("red")).toBe("\x1b[31m");
|
|
36
|
+
expect(resolveStyle("blue")).toBe("\x1b[34m");
|
|
37
|
+
});
|
|
38
|
+
test("resolveStyle handles modifiers", () => {
|
|
39
|
+
expect(resolveStyle("bold")).toBe("\x1b[1m");
|
|
40
|
+
expect(resolveStyle("reset")).toBe("\x1b[0m");
|
|
41
|
+
});
|
|
42
|
+
test("resolveStyle handles hex colors", () => {
|
|
43
|
+
expect(resolveStyle("#FF0000")).toBe("\x1b[38;2;255;0;0m");
|
|
44
|
+
});
|
|
45
|
+
test("resolveStyle handles arrays of styles", () => {
|
|
46
|
+
expect(resolveStyle(["bold", "red"])).toBe("\x1b[1m\x1b[31m");
|
|
47
|
+
});
|
|
48
|
+
test("resolveStyle passes through raw strings", () => {
|
|
49
|
+
const raw = "\x1b[31m";
|
|
50
|
+
expect(resolveStyle(raw)).toBe(raw);
|
|
51
|
+
expect(resolveStyle("unknown")).toBe("unknown");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("Line Utilities", () => {
|
|
55
|
+
test("getLineLength calculates correctly", () => {
|
|
56
|
+
expect(getLineLength(lineA)).toBe(5);
|
|
57
|
+
expect(getLineLength(lineB)).toBe(7);
|
|
58
|
+
});
|
|
59
|
+
test("computeMaxWidth finds the longest line", () => {
|
|
60
|
+
expect(computeMaxWidth([lineA, lineB])).toBe(7);
|
|
61
|
+
expect(computeMaxWidth([])).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
test("padLine adds padding when needed", () => {
|
|
64
|
+
const padded = padLine(lineA, 10, "red");
|
|
65
|
+
expect(getLineLength(padded)).toBe(10);
|
|
66
|
+
expect(padded.segments[1].style).toBe("red");
|
|
67
|
+
expect(padded.segments[1].text).toBe(" ");
|
|
68
|
+
});
|
|
69
|
+
test("padLine does nothing if line is already wide enough", () => {
|
|
70
|
+
const ignored = padLine(lineB, 5, "red");
|
|
71
|
+
expect(getLineLength(ignored)).toBe(7);
|
|
72
|
+
expect(ignored.segments.length).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
test("mergeColumns handles asymmetric column lengths", () => {
|
|
75
|
+
const merged = mergeColumns([lineA], [lineB, lineB], 10, " | ", "");
|
|
76
|
+
expect(merged.length).toBe(2);
|
|
77
|
+
expect(getLineLength(merged[1])).toBe(10 + 3 + 7);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("Printer & Layout", () => {
|
|
81
|
+
const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
stdoutSpy.mockClear();
|
|
84
|
+
});
|
|
85
|
+
test("Printer.print outputs to console with resolved styles", () => {
|
|
86
|
+
const printer = new Printer();
|
|
87
|
+
printer.print([{ segments: [{ text: "Test", style: "red" }] }]);
|
|
88
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
89
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
90
|
+
expect(output).toContain("\x1b[31mTest");
|
|
91
|
+
});
|
|
92
|
+
test("Printer handles interactive clearing", () => {
|
|
93
|
+
const printer = new Printer({ interactive: true });
|
|
94
|
+
printer.print([{ segments: [{ text: "L1", style: "" }] }]);
|
|
95
|
+
printer.print([{ segments: [{ text: "L2", style: "" }] }]);
|
|
96
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining("\x1b[1A\x1b[2K\r"));
|
|
97
|
+
});
|
|
98
|
+
test("Printer optimizes clearing of multiple lines", () => {
|
|
99
|
+
const printer = new Printer({ interactive: true });
|
|
100
|
+
printer.print([
|
|
101
|
+
{ segments: [{ text: "L1", style: "" }] },
|
|
102
|
+
{ segments: [{ text: "L2", style: "" }] },
|
|
103
|
+
{ segments: [{ text: "L3", style: "" }] }
|
|
104
|
+
]);
|
|
105
|
+
stdoutSpy.mockClear();
|
|
106
|
+
printer.print([{ segments: [{ text: "New", style: "" }] }]);
|
|
107
|
+
const clearSeq = "\x1b[1A\x1b[2K\r";
|
|
108
|
+
const expectedClear = clearSeq.repeat(3);
|
|
109
|
+
expect(stdoutSpy).toHaveBeenCalledTimes(1);
|
|
110
|
+
const callArg = stdoutSpy.mock.calls[0][0];
|
|
111
|
+
expect(callArg.startsWith(expectedClear)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
test("printDualColumn executes correctly", () => {
|
|
114
|
+
printDualColumn([lineA], [lineB]);
|
|
115
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
test("getDragonLines returns valid array", () => {
|
|
118
|
+
const lines = getDragonLines();
|
|
119
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
120
|
+
const firstSegmentStyle = lines[0].segments[0].style;
|
|
121
|
+
expect(firstSegmentStyle.startsWith("#")).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { StyledLine, Style } from "./index";
|
|
2
|
+
export interface ProgressBarOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Progress value between 0.0 and 1.0.
|
|
5
|
+
*/
|
|
6
|
+
progress: number;
|
|
7
|
+
/**
|
|
8
|
+
* Width of the progress bar (excluding brackets and percentage).
|
|
9
|
+
* Defaults to 20.
|
|
10
|
+
*/
|
|
11
|
+
width?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Base style for the entire progress bar.
|
|
14
|
+
*/
|
|
15
|
+
style?: Style | Style[];
|
|
16
|
+
/**
|
|
17
|
+
* Style for the brackets (start and end characters).
|
|
18
|
+
* Defaults to `style` or gray.
|
|
19
|
+
*/
|
|
20
|
+
bracketStyle?: Style | Style[];
|
|
21
|
+
/**
|
|
22
|
+
* Specific style for the start bracket. Overrides `bracketStyle`.
|
|
23
|
+
*/
|
|
24
|
+
startStyle?: Style | Style[];
|
|
25
|
+
/**
|
|
26
|
+
* Specific style for the end bracket. Overrides `bracketStyle`.
|
|
27
|
+
*/
|
|
28
|
+
endStyle?: Style | Style[];
|
|
29
|
+
/**
|
|
30
|
+
* Style for the bar (filled and empty parts).
|
|
31
|
+
* Defaults to `style`.
|
|
32
|
+
*/
|
|
33
|
+
barStyle?: Style | Style[];
|
|
34
|
+
/**
|
|
35
|
+
* Specific style for the filled part. Overrides `barStyle`.
|
|
36
|
+
*/
|
|
37
|
+
fillStyle?: Style | Style[];
|
|
38
|
+
/**
|
|
39
|
+
* Specific style for the empty part. Overrides `barStyle`.
|
|
40
|
+
*/
|
|
41
|
+
emptyStyle?: Style | Style[];
|
|
42
|
+
/**
|
|
43
|
+
* Style for the percentage text.
|
|
44
|
+
* Defaults to `style`.
|
|
45
|
+
*/
|
|
46
|
+
percentageStyle?: Style | Style[];
|
|
47
|
+
/**
|
|
48
|
+
* Character to use for the start bracket. Defaults to `[`.
|
|
49
|
+
*/
|
|
50
|
+
startChar?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Character to use for the end bracket. Defaults to `]`.
|
|
53
|
+
*/
|
|
54
|
+
endChar?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Character to use for the filled part. Defaults to `█`.
|
|
57
|
+
*/
|
|
58
|
+
fillChar?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Character to use for the empty part. Defaults to `░`.
|
|
61
|
+
*/
|
|
62
|
+
emptyChar?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether to show the percentage text. Defaults to `true`.
|
|
65
|
+
*/
|
|
66
|
+
showPercentage?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Custom formatter for the percentage text.
|
|
69
|
+
*/
|
|
70
|
+
formatPercentage?: (progress: number) => string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Creates a StyledLine representing a progress bar.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createProgressBar(options: ProgressBarOptions): StyledLine;
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a StyledLine representing a progress bar.
|
|
3
|
+
*/
|
|
4
|
+
export function createProgressBar(options) {
|
|
5
|
+
const { progress, width = 20, style, bracketStyle, startStyle, endStyle, barStyle, fillStyle, emptyStyle, percentageStyle, startChar = "[", endChar = "]", fillChar = "█", emptyChar = "░", showPercentage = true, formatPercentage } = options;
|
|
6
|
+
// Clamp progress
|
|
7
|
+
const p = Math.max(0, Math.min(1, progress));
|
|
8
|
+
// Calculate filled width
|
|
9
|
+
const filledWidth = Math.round(p * width);
|
|
10
|
+
const emptyWidth = width - filledWidth;
|
|
11
|
+
// Resolve styles
|
|
12
|
+
const baseStyle = style ?? [];
|
|
13
|
+
const resolvedBracketStyle = bracketStyle ?? baseStyle;
|
|
14
|
+
const resolvedStartStyle = startStyle ?? resolvedBracketStyle;
|
|
15
|
+
const resolvedEndStyle = endStyle ?? resolvedBracketStyle;
|
|
16
|
+
const resolvedBarStyle = barStyle ?? baseStyle;
|
|
17
|
+
const resolvedFillStyle = fillStyle ?? resolvedBarStyle;
|
|
18
|
+
const resolvedEmptyStyle = emptyStyle ?? resolvedBarStyle;
|
|
19
|
+
const resolvedPercentageStyle = percentageStyle ?? baseStyle;
|
|
20
|
+
const segments = [];
|
|
21
|
+
// Start Bracket
|
|
22
|
+
if (startChar) {
|
|
23
|
+
segments.push({ text: startChar, style: resolvedStartStyle });
|
|
24
|
+
}
|
|
25
|
+
// Filled Part
|
|
26
|
+
if (filledWidth > 0) {
|
|
27
|
+
segments.push({ text: fillChar.repeat(filledWidth), style: resolvedFillStyle });
|
|
28
|
+
}
|
|
29
|
+
// Empty Part
|
|
30
|
+
if (emptyWidth > 0) {
|
|
31
|
+
segments.push({ text: emptyChar.repeat(emptyWidth), style: resolvedEmptyStyle });
|
|
32
|
+
}
|
|
33
|
+
// End Bracket
|
|
34
|
+
if (endChar) {
|
|
35
|
+
segments.push({ text: endChar, style: resolvedEndStyle });
|
|
36
|
+
}
|
|
37
|
+
// Percentage
|
|
38
|
+
if (showPercentage) {
|
|
39
|
+
const percentageText = formatPercentage ? formatPercentage(p) : ` ${Math.round(p * 100)}%`;
|
|
40
|
+
segments.push({ text: percentageText, style: resolvedPercentageStyle });
|
|
41
|
+
}
|
|
42
|
+
return { segments };
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { createProgressBar } from "./progress";
|
|
3
|
+
function getText(line) {
|
|
4
|
+
return line.segments.map((s) => s.text).join("");
|
|
5
|
+
}
|
|
6
|
+
describe("createProgressBar", () => {
|
|
7
|
+
test("generates a default progress bar at 0%", () => {
|
|
8
|
+
const line = createProgressBar({ progress: 0 });
|
|
9
|
+
const text = getText(line);
|
|
10
|
+
expect(text).toContain("[");
|
|
11
|
+
expect(text).toContain("]");
|
|
12
|
+
expect(text).toContain("0%");
|
|
13
|
+
expect(text).toContain("░");
|
|
14
|
+
});
|
|
15
|
+
test("generates a default progress bar at 50%", () => {
|
|
16
|
+
const line = createProgressBar({ progress: 0.5 });
|
|
17
|
+
const text = getText(line);
|
|
18
|
+
expect(text).toContain("50%");
|
|
19
|
+
expect(text).toContain("█");
|
|
20
|
+
expect(text).toContain("░");
|
|
21
|
+
});
|
|
22
|
+
test("generates a default progress bar at 100%", () => {
|
|
23
|
+
const line = createProgressBar({ progress: 1.0 });
|
|
24
|
+
const text = getText(line);
|
|
25
|
+
expect(text).toContain("100%");
|
|
26
|
+
expect(text).not.toContain("░");
|
|
27
|
+
});
|
|
28
|
+
test("allows custom width", () => {
|
|
29
|
+
const width = 10;
|
|
30
|
+
const line = createProgressBar({ progress: 0.5, width });
|
|
31
|
+
const segments = line.segments;
|
|
32
|
+
const filled = segments.find((s) => s.text.includes("█"));
|
|
33
|
+
const empty = segments.find((s) => s.text.includes("░"));
|
|
34
|
+
expect(filled?.text.length).toBe(5);
|
|
35
|
+
expect(empty?.text.length).toBe(5);
|
|
36
|
+
});
|
|
37
|
+
test("applies styles correctly", () => {
|
|
38
|
+
const line = createProgressBar({
|
|
39
|
+
progress: 0.5,
|
|
40
|
+
style: "blue",
|
|
41
|
+
bracketStyle: "red",
|
|
42
|
+
barStyle: "green",
|
|
43
|
+
percentageStyle: "yellow"
|
|
44
|
+
});
|
|
45
|
+
const start = line.segments.find((s) => s.text === "[");
|
|
46
|
+
const filled = line.segments.find((s) => s.text.includes("█"));
|
|
47
|
+
const end = line.segments.find((s) => s.text === "]");
|
|
48
|
+
const percentage = line.segments.find((s) => s.text.includes("%"));
|
|
49
|
+
expect(start?.style).toBe("red");
|
|
50
|
+
expect(filled?.style).toBe("green");
|
|
51
|
+
expect(end?.style).toBe("red");
|
|
52
|
+
expect(percentage?.style).toBe("yellow");
|
|
53
|
+
});
|
|
54
|
+
test("cascades styles (general style -> specific)", () => {
|
|
55
|
+
const line = createProgressBar({
|
|
56
|
+
progress: 0.5,
|
|
57
|
+
style: "blue"
|
|
58
|
+
});
|
|
59
|
+
line.segments.forEach((s) => {
|
|
60
|
+
if (s.text.trim().length > 0) {
|
|
61
|
+
expect(s.style).toBe("blue");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
test("allows custom characters", () => {
|
|
66
|
+
const line = createProgressBar({
|
|
67
|
+
progress: 0.5,
|
|
68
|
+
startChar: "<",
|
|
69
|
+
endChar: ">",
|
|
70
|
+
fillChar: "=",
|
|
71
|
+
emptyChar: "-"
|
|
72
|
+
});
|
|
73
|
+
const text = getText(line);
|
|
74
|
+
expect(text).toContain("<");
|
|
75
|
+
expect(text).toContain(">");
|
|
76
|
+
expect(text).toContain("=");
|
|
77
|
+
expect(text).toContain("-");
|
|
78
|
+
});
|
|
79
|
+
test("hides percentage", () => {
|
|
80
|
+
const line = createProgressBar({
|
|
81
|
+
progress: 0.5,
|
|
82
|
+
showPercentage: false
|
|
83
|
+
});
|
|
84
|
+
const text = getText(line);
|
|
85
|
+
expect(text).not.toContain("%");
|
|
86
|
+
});
|
|
87
|
+
test("formats percentage custom", () => {
|
|
88
|
+
const line = createProgressBar({
|
|
89
|
+
progress: 0.5,
|
|
90
|
+
formatPercentage: (p) => `${p * 10}/10`
|
|
91
|
+
});
|
|
92
|
+
const text = getText(line);
|
|
93
|
+
expect(text).toContain("5/10");
|
|
94
|
+
});
|
|
95
|
+
test("clamping progress", () => {
|
|
96
|
+
const lineLow = createProgressBar({ progress: -0.1 });
|
|
97
|
+
expect(getText(lineLow)).toContain("0%");
|
|
98
|
+
const lineHigh = createProgressBar({ progress: 1.1 });
|
|
99
|
+
expect(getText(lineHigh)).toContain("100%");
|
|
100
|
+
});
|
|
101
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heinrichb/console-toolkit",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A versatile TypeScript utility library for enhanced console logging, formatting, and layout management.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"demo": "bun run scripts/run-demo.ts",
|
|
17
|
+
"test": "bun run build && bun test src",
|
|
18
|
+
"test:coverage": "bun run build && bun test src --coverage --coverage-threshold=100",
|
|
19
|
+
"build": "bun x tsc",
|
|
20
|
+
"publish": "npm publish",
|
|
21
|
+
"format:check": "prettier --check \"src/**/*.{ts,json,md}\"",
|
|
22
|
+
"format:write": "prettier --write \"src/**/*.{ts,json,md}\"",
|
|
23
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
24
|
+
"tree": "tree -a -I '.git|node_modules|dist'"
|
|
25
|
+
},
|
|
26
|
+
"prettier": {
|
|
27
|
+
"printWidth": 128,
|
|
28
|
+
"tabWidth": 2,
|
|
29
|
+
"trailingComma": "none",
|
|
30
|
+
"bracketSameLine": true,
|
|
31
|
+
"semi": true,
|
|
32
|
+
"singleQuote": false,
|
|
33
|
+
"useTabs": true
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^9.0.0",
|
|
37
|
+
"@types/bun": "latest",
|
|
38
|
+
"eslint": "^9.0.0",
|
|
39
|
+
"eslint-config-prettier": "^10.1.8",
|
|
40
|
+
"prettier": "^3.5.3",
|
|
41
|
+
"typescript": "^5",
|
|
42
|
+
"typescript-eslint": "^8.56.1"
|
|
43
|
+
}
|
|
44
|
+
}
|