fix JSR requirements
This commit is contained in:
parent
acdad1b233
commit
1a6ac2b79f
19 changed files with 1257 additions and 1081 deletions
12
deno.jsonc
Normal file
12
deno.jsonc
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "@clo/console",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/console.ts",
|
||||||
|
"./Spinner": "./src/Spinner.ts",
|
||||||
|
"./Progress": "./src/Progress.ts",
|
||||||
|
"./Widget": "./src/Widget.ts",
|
||||||
|
"./inject": "./src/inject.ts",
|
||||||
|
"./error": "./src/error.ts"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import * as console from '@paperclover/console';
|
import * as console from "@paperclover/console";
|
||||||
|
|
||||||
console.info('Hello, world!');
|
console.info("Hello, world!");
|
||||||
console.warn('This is a warning');
|
console.warn("This is a warning");
|
||||||
console.error('This is an error');
|
console.error("This is an error");
|
||||||
console.debug('This is a debug message');
|
console.debug("This is a debug message");
|
||||||
console.success('This is a success message');
|
console.success("This is a success message");
|
||||||
|
|
||||||
const custom = console.scoped('my_own');
|
const custom = console.scoped("my_own");
|
||||||
custom('Hello, world!');
|
custom("Hello, world!");
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import '@paperclover/console/inject';
|
import "@paperclover/console/inject";
|
||||||
|
|
||||||
console.info('Hello, world!');
|
console.info("Hello, world!");
|
||||||
console.warn('This is a warning');
|
console.warn("This is a warning");
|
||||||
console.error('This is an error');
|
console.error("This is an error");
|
||||||
console.debug('This is a debug message');
|
console.debug("This is a debug message");
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { withProgress } from "@paperclover/console/Progress";
|
import { withProgress } from "@paperclover/console/Progress";
|
||||||
|
|
||||||
await withProgress('do a task', async (progress) => {
|
await withProgress("do a task", async (progress) => {
|
||||||
// mutate the progress object
|
// mutate the progress object
|
||||||
progress.total = 100;
|
progress.total = 100;
|
||||||
|
|
||||||
for (let i = 0; i < progress.total; i++) {
|
for (let i = 0; i < progress.total; i++) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
progress.value += 1;
|
progress.value += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
import { Spinner } from "@paperclover/console/Spinner";
|
import { Spinner } from "@paperclover/console/Spinner";
|
||||||
|
|
||||||
const first = new Spinner({
|
const first = new Spinner({
|
||||||
text: 'Spinner 1: ',
|
text: "Spinner 1: ",
|
||||||
color: 'blueBright',
|
color: "blueBright",
|
||||||
});
|
});
|
||||||
|
|
||||||
const second = new Spinner({
|
const second = new Spinner({
|
||||||
text: () => `Spinner 2: ${random()}`,
|
text: () => `Spinner 2: ${random()}`,
|
||||||
color: 'blueBright',
|
color: "blueBright",
|
||||||
});
|
});
|
||||||
second.fps = 30;
|
second.fps = 30;
|
||||||
|
|
||||||
const third = new Spinner<{ value: string }>({
|
const third = new Spinner<{ value: string }>({
|
||||||
text: ({ value }) => `Spinner 3: ${value}`,
|
text: ({ value }) => `Spinner 3: ${value}`,
|
||||||
color: 'blueBright',
|
color: "blueBright",
|
||||||
});
|
});
|
||||||
third.fps = 4;
|
third.fps = 4;
|
||||||
|
|
||||||
for (let i = 0; i < 40; i++) {
|
for (let i = 0; i < 40; i++) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
first.text = `Spinner 1: ${random()}`;
|
first.text = `Spinner 1: ${random()}`;
|
||||||
if (i === 20) {
|
if (i === 20) {
|
||||||
second.success('second done!');
|
second.success("second done!");
|
||||||
}
|
}
|
||||||
third.update({ value: random() });
|
third.update({ value: random() });
|
||||||
}
|
}
|
||||||
|
|
||||||
first.success('first done!');
|
first.success("first done!");
|
||||||
// third.success('third done!');
|
// third.success('third done!');
|
||||||
|
|
||||||
function random() {
|
function random() {
|
||||||
return Math.random().toString(36).substring(2, 15);
|
return Math.random().toString(36).substring(2, 15);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,336 +1,349 @@
|
||||||
import { beforeEach, expect, test, mock } from "bun:test";
|
import { beforeEach, expect, mock, test } from "bun:test";
|
||||||
import { reset, expectCalls, getWidgets } from "./test/widget-mock.ts";
|
import { expectCalls, getWidgets, reset } from "./test/widget-mock.ts";
|
||||||
|
|
||||||
const { Progress, withProgress } = await import("@paperclover/console/Progress");
|
const { Progress, withProgress } = await import(
|
||||||
type Progress<T extends Record<string, unknown> = Record<never, unknown>> = import("@paperclover/console/Progress").Progress<T>;
|
"@paperclover/console/Progress"
|
||||||
|
);
|
||||||
|
type Progress<T extends Record<string, unknown> = Record<never, unknown>> =
|
||||||
|
import("@paperclover/console/Progress").Progress<T>;
|
||||||
|
|
||||||
beforeEach(reset);
|
beforeEach(reset);
|
||||||
|
|
||||||
test("default options and basic rendering", () => {
|
test("default options and basic rendering", () => {
|
||||||
const progress = new Progress("Loading");
|
const progress = new Progress("Loading");
|
||||||
|
|
||||||
expect(progress.text).toEqual("Loading");
|
expect(progress.text).toEqual("Loading");
|
||||||
expect(progress.beforeText).toEqual("");
|
expect(progress.beforeText).toEqual("");
|
||||||
expect(progress.value).toEqual(0);
|
expect(progress.value).toEqual(0);
|
||||||
expect(progress.total).toEqual(100);
|
expect(progress.total).toEqual(100);
|
||||||
|
|
||||||
// Format should include a progress bar
|
// Format should include a progress bar
|
||||||
const formatted = progress.format(0);
|
const formatted = progress.format(0);
|
||||||
expect(formatted).toContain("Loading");
|
expect(formatted).toContain("Loading");
|
||||||
// The progress bar might not contain "█" at 0% progress,
|
// The progress bar might not contain "█" at 0% progress,
|
||||||
// but it should contain color codes and spaces for the bar
|
// but it should contain color codes and spaces for the bar
|
||||||
expect(formatted).toContain("\u001b[38;2;"); // Color code
|
expect(formatted).toContain("\u001b[38;2;"); // Color code
|
||||||
});
|
});
|
||||||
|
|
||||||
test("progress percentage displays correctly", () => {
|
test("progress percentage displays correctly", () => {
|
||||||
const progress = new Progress("Loading");
|
const progress = new Progress("Loading");
|
||||||
|
|
||||||
// Test zero progress
|
// Test zero progress
|
||||||
progress.value = 0;
|
progress.value = 0;
|
||||||
expect(progress.format(0)).toContain("Loading");
|
expect(progress.format(0)).toContain("Loading");
|
||||||
|
|
||||||
// Test partial progress
|
// Test partial progress
|
||||||
progress.value = 50;
|
progress.value = 50;
|
||||||
const halfProgress = progress.format(0);
|
const halfProgress = progress.format(0);
|
||||||
expect(halfProgress).toContain("Loading");
|
expect(halfProgress).toContain("Loading");
|
||||||
|
|
||||||
// Test full progress
|
// Test full progress
|
||||||
progress.value = 100;
|
progress.value = 100;
|
||||||
const fullProgress = progress.format(0);
|
const fullProgress = progress.format(0);
|
||||||
expect(fullProgress).toContain("Loading");
|
expect(fullProgress).toContain("Loading");
|
||||||
|
|
||||||
// Should be visibly different
|
// Should be visibly different
|
||||||
expect(progress.format(0)).not.toEqual(halfProgress);
|
expect(progress.format(0)).not.toEqual(halfProgress);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("custom options apply correctly", () => {
|
test("custom options apply correctly", () => {
|
||||||
const progress = new Progress({
|
const progress = new Progress({
|
||||||
text: "Custom Progress",
|
text: "Custom Progress",
|
||||||
beforeText: "Loading:",
|
beforeText: "Loading:",
|
||||||
barWidth: 20,
|
barWidth: 20,
|
||||||
barStyle: "ascii",
|
barStyle: "ascii",
|
||||||
value: 25,
|
value: 25,
|
||||||
total: 200
|
total: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(progress.text).toEqual("Custom Progress");
|
expect(progress.text).toEqual("Custom Progress");
|
||||||
expect(progress.beforeText).toEqual("Loading:");
|
expect(progress.beforeText).toEqual("Loading:");
|
||||||
expect(progress.value).toEqual(25);
|
expect(progress.value).toEqual(25);
|
||||||
expect(progress.total).toEqual(200);
|
expect(progress.total).toEqual(200);
|
||||||
|
|
||||||
const formatted = progress.format(0);
|
const formatted = progress.format(0);
|
||||||
expect(formatted).toContain("Custom Progress");
|
expect(formatted).toContain("Custom Progress");
|
||||||
expect(formatted).toContain("Loading:");
|
expect(formatted).toContain("Loading:");
|
||||||
// With ascii style, it should contain these characters
|
// With ascii style, it should contain these characters
|
||||||
expect(formatted).toContain("[");
|
expect(formatted).toContain("[");
|
||||||
expect(formatted).toContain("]");
|
expect(formatted).toContain("]");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("spinner options", () => {
|
test("spinner options", () => {
|
||||||
// Test with custom spinner
|
// Test with custom spinner
|
||||||
const customSpinnerProgress = new Progress({
|
const customSpinnerProgress = new Progress({
|
||||||
text: "With spinner",
|
text: "With spinner",
|
||||||
spinner: {
|
spinner: {
|
||||||
frames: ["A", "B", "C"],
|
frames: ["A", "B", "C"],
|
||||||
fps: 10,
|
fps: 10,
|
||||||
color: "red"
|
color: "red",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(customSpinnerProgress.fps).toBeGreaterThan(0); // Should have fps when spinner is enabled
|
expect(customSpinnerProgress.fps).toBeGreaterThan(0); // Should have fps when spinner is enabled
|
||||||
const formatted = customSpinnerProgress.format(0);
|
const formatted = customSpinnerProgress.format(0);
|
||||||
expect(formatted).toContain("A"); // Should include the first spinner frame
|
expect(formatted).toContain("A"); // Should include the first spinner frame
|
||||||
|
|
||||||
// Test with spinner disabled
|
// Test with spinner disabled
|
||||||
const noSpinnerProgress = new Progress({
|
const noSpinnerProgress = new Progress({
|
||||||
text: "No spinner",
|
text: "No spinner",
|
||||||
spinner: null
|
spinner: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(noSpinnerProgress.fps).toBe(0); // Should have 0 fps when spinner is disabled
|
expect(noSpinnerProgress.fps).toBe(0); // Should have 0 fps when spinner is disabled
|
||||||
const noSpinnerFormatted = noSpinnerProgress.format(0);
|
const noSpinnerFormatted = noSpinnerProgress.format(0);
|
||||||
expect(noSpinnerFormatted).not.toContain("A");
|
expect(noSpinnerFormatted).not.toContain("A");
|
||||||
expect(noSpinnerFormatted).not.toContain("B");
|
expect(noSpinnerFormatted).not.toContain("B");
|
||||||
expect(noSpinnerFormatted).not.toContain("C");
|
expect(noSpinnerFormatted).not.toContain("C");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("text getter and setter", () => {
|
test("text getter and setter", () => {
|
||||||
const progress = new Progress("Initial text");
|
const progress = new Progress("Initial text");
|
||||||
expect(progress.text).toEqual("Initial text");
|
expect(progress.text).toEqual("Initial text");
|
||||||
|
|
||||||
progress.text = "Updated text";
|
progress.text = "Updated text";
|
||||||
expect(progress.text).toEqual("Updated text");
|
expect(progress.text).toEqual("Updated text");
|
||||||
|
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("beforeText getter and setter", () => {
|
test("beforeText getter and setter", () => {
|
||||||
const progress = new Progress({
|
const progress = new Progress({
|
||||||
text: "Main text",
|
text: "Main text",
|
||||||
beforeText: "Initial before"
|
beforeText: "Initial before",
|
||||||
});
|
});
|
||||||
expect(progress.beforeText).toEqual("Initial before");
|
expect(progress.beforeText).toEqual("Initial before");
|
||||||
|
|
||||||
progress.beforeText = "Updated before";
|
progress.beforeText = "Updated before";
|
||||||
expect(progress.beforeText).toEqual("Updated before");
|
expect(progress.beforeText).toEqual("Updated before");
|
||||||
|
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("value and total getters and setters", () => {
|
test("value and total getters and setters", () => {
|
||||||
const progress = new Progress("Progress");
|
const progress = new Progress("Progress");
|
||||||
expect(progress.value).toEqual(0);
|
expect(progress.value).toEqual(0);
|
||||||
expect(progress.total).toEqual(100);
|
expect(progress.total).toEqual(100);
|
||||||
|
|
||||||
progress.value = 25;
|
progress.value = 25;
|
||||||
expect(progress.value).toEqual(25);
|
expect(progress.value).toEqual(25);
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
progress.total = 200;
|
progress.total = 200;
|
||||||
expect(progress.total).toEqual(200);
|
expect(progress.total).toEqual(200);
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
redraws: 2
|
redraws: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("function for text field and props", () => {
|
test("function for text field and props", () => {
|
||||||
const textFn = (props: { value: number; total: number; progress: number; customProp: string }) =>
|
const textFn = (
|
||||||
`${props.customProp}: ${props.value}/${props.total} (${Math.round(props.progress * 100)}%)`;
|
props: {
|
||||||
|
value: number;
|
||||||
|
total: number;
|
||||||
|
progress: number;
|
||||||
|
customProp: string;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
`${props.customProp}: ${props.value}/${props.total} (${
|
||||||
|
Math.round(props.progress * 100)
|
||||||
|
}%)`;
|
||||||
|
|
||||||
const progress = new Progress<{ customProp: string }>({
|
const progress = new Progress<{ customProp: string }>({
|
||||||
text: textFn,
|
text: textFn,
|
||||||
props: { customProp: "Loading" },
|
props: { customProp: "Loading" },
|
||||||
value: 30,
|
value: 30,
|
||||||
total: 120
|
total: 120,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Text function should use props and calculate progress
|
// Text function should use props and calculate progress
|
||||||
expect(progress.text).toEqual("Loading: 30/120 (25%)");
|
expect(progress.text).toEqual("Loading: 30/120 (25%)");
|
||||||
|
|
||||||
// Updating props should reflect in text
|
// Updating props should reflect in text
|
||||||
progress.props = { customProp: "Downloading" };
|
progress.props = { customProp: "Downloading" };
|
||||||
expect(progress.text).toEqual("Downloading: 30/120 (25%)");
|
expect(progress.text).toEqual("Downloading: 30/120 (25%)");
|
||||||
|
|
||||||
// Updating value should reflect in text
|
// Updating value should reflect in text
|
||||||
progress.value = 60;
|
progress.value = 60;
|
||||||
expect(progress.text).toEqual("Downloading: 60/120 (50%)");
|
expect(progress.text).toEqual("Downloading: 60/120 (50%)");
|
||||||
|
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
redraws: 2
|
redraws: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update method and its overloads", () => {
|
test("update method and its overloads", () => {
|
||||||
const progress = new Progress<{ status: string }>({
|
const progress = new Progress<{ status: string }>({
|
||||||
text: (props) => `${props.status}: ${props.value}/${props.total}`,
|
text: (props) => `${props.status}: ${props.value}/${props.total}`,
|
||||||
props: { status: "Loading" }
|
props: { status: "Loading" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
expect(progress.text).toEqual("Loading: 0/100");
|
expect(progress.text).toEqual("Loading: 0/100");
|
||||||
|
|
||||||
// Update with just value
|
// Update with just value
|
||||||
progress.update(50);
|
progress.update(50);
|
||||||
expect(progress.value).toEqual(50);
|
expect(progress.value).toEqual(50);
|
||||||
expect(progress.text).toEqual("Loading: 50/100");
|
expect(progress.text).toEqual("Loading: 50/100");
|
||||||
|
|
||||||
// Update with value and props
|
// Update with value and props
|
||||||
progress.update(75, { status: "Downloading" });
|
progress.update(75, { status: "Downloading" });
|
||||||
expect(progress.value).toEqual(75);
|
expect(progress.value).toEqual(75);
|
||||||
expect(progress.text).toEqual("Downloading: 75/100");
|
expect(progress.text).toEqual("Downloading: 75/100");
|
||||||
|
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
redraws: 2
|
redraws: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("success and error methods", () => {
|
test("success and error methods", () => {
|
||||||
const progress = new Progress("Working");
|
const progress = new Progress("Working");
|
||||||
|
|
||||||
// Test success with default message
|
// Test success with default message
|
||||||
progress.success();
|
progress.success();
|
||||||
expectCalls(progress, {
|
expectCalls(progress, {
|
||||||
successCalls: ["Working"]
|
successCalls: ["Working"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test success with custom message
|
// Test success with custom message
|
||||||
const progress2 = new Progress("Working");
|
const progress2 = new Progress("Working");
|
||||||
progress2.success("Completed");
|
progress2.success("Completed");
|
||||||
expectCalls(progress2, {
|
expectCalls(progress2, {
|
||||||
successCalls: ["Completed"]
|
successCalls: ["Completed"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test error with default message
|
// Test error with default message
|
||||||
const progress3 = new Progress("Working");
|
const progress3 = new Progress("Working");
|
||||||
progress3.error();
|
progress3.error();
|
||||||
expectCalls(progress3, {
|
expectCalls(progress3, {
|
||||||
errorCalls: ["Working"]
|
errorCalls: ["Working"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test error with custom message
|
// Test error with custom message
|
||||||
const progress4 = new Progress("Working");
|
const progress4 = new Progress("Working");
|
||||||
progress4.error("Failed");
|
progress4.error("Failed");
|
||||||
expectCalls(progress4, {
|
expectCalls(progress4, {
|
||||||
errorCalls: ["Failed"]
|
errorCalls: ["Failed"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test error with Error object
|
// Test error with Error object
|
||||||
const error = new Error("Something went wrong");
|
const error = new Error("Something went wrong");
|
||||||
const progress5 = new Progress("Working");
|
const progress5 = new Progress("Working");
|
||||||
progress5.error(error);
|
progress5.error(error);
|
||||||
expectCalls(progress5, {
|
expectCalls(progress5, {
|
||||||
errorCalls: [error]
|
errorCalls: [error],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withProgress function", async () => {
|
test("withProgress function", async () => {
|
||||||
const expectedResult = Symbol("result");
|
const expectedResult = Symbol("result");
|
||||||
let bar!: Progress;
|
let bar!: Progress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// We need to use try/finally to ensure we can access bar for validation
|
// We need to use try/finally to ensure we can access bar for validation
|
||||||
await withProgress("Processing", async (progress) => {
|
await withProgress("Processing", async (progress) => {
|
||||||
bar = progress;
|
bar = progress;
|
||||||
expect(progress.text).toEqual("Processing");
|
expect(progress.text).toEqual("Processing");
|
||||||
|
|
||||||
// Update progress during operation
|
// Update progress during operation
|
||||||
progress.value = 50;
|
progress.value = 50;
|
||||||
|
|
||||||
return expectedResult;
|
return expectedResult;
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
expectCalls(bar, {
|
expectCalls(bar, {
|
||||||
successCalls: ["Completed"],
|
successCalls: ["Completed"],
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withProgress function with custom success text", async () => {
|
test("withProgress function with custom success text", async () => {
|
||||||
let bar!: Progress;
|
let bar!: Progress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withProgress({
|
await withProgress({
|
||||||
text: "Processing",
|
text: "Processing",
|
||||||
successText: "Task completed successfully"
|
successText: "Task completed successfully",
|
||||||
}, async (progress) => {
|
}, async (progress) => {
|
||||||
bar = progress;
|
bar = progress;
|
||||||
return "data";
|
return "data";
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
expectCalls(bar, {
|
expectCalls(bar, {
|
||||||
successCalls: ["Task completed successfully"],
|
successCalls: ["Task completed successfully"],
|
||||||
redraws: 0
|
redraws: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withProgress function with success text function", async () => {
|
test("withProgress function with success text function", async () => {
|
||||||
let bar!: Progress;
|
let bar!: Progress;
|
||||||
const data = { count: 42 };
|
const data = { count: 42 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withProgress({
|
await withProgress({
|
||||||
text: "Processing",
|
text: "Processing",
|
||||||
successText: (result: { count: number }) => `Processed ${result.count} items`
|
successText: (result: { count: number }) =>
|
||||||
}, async (progress) => {
|
`Processed ${result.count} items`,
|
||||||
bar = progress;
|
}, async (progress) => {
|
||||||
return data;
|
bar = progress;
|
||||||
});
|
return data;
|
||||||
} finally {
|
});
|
||||||
expectCalls(bar, {
|
} finally {
|
||||||
successCalls: ["Processed 42 items"],
|
expectCalls(bar, {
|
||||||
redraws: 0
|
successCalls: ["Processed 42 items"],
|
||||||
});
|
redraws: 0,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withProgress function with error", async () => {
|
test("withProgress function with error", async () => {
|
||||||
const error = new Error("Process failed");
|
const error = new Error("Process failed");
|
||||||
let bar!: Progress;
|
let bar!: Progress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withProgress({
|
await withProgress({
|
||||||
text: "Processing",
|
text: "Processing",
|
||||||
failureText: "Task failed"
|
failureText: "Task failed",
|
||||||
}, async (progress) => {
|
}, async (progress) => {
|
||||||
bar = progress;
|
bar = progress;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
expect.unreachable();
|
expect.unreachable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
expectCalls(bar, {
|
expectCalls(bar, {
|
||||||
errorCalls: ["Task failed"],
|
errorCalls: ["Task failed"],
|
||||||
redraws: 0
|
redraws: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withProgress function with error text function", async () => {
|
test("withProgress function with error text function", async () => {
|
||||||
const error = new Error("Process failed with code 500");
|
const error = new Error("Process failed with code 500");
|
||||||
let bar!: Progress;
|
let bar!: Progress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withProgress({
|
await withProgress({
|
||||||
text: "Processing",
|
text: "Processing",
|
||||||
failureText: (err) => `Error: ${err.message}`
|
failureText: (err) => `Error: ${err.message}`,
|
||||||
}, async (progress) => {
|
}, async (progress) => {
|
||||||
bar = progress;
|
bar = progress;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
expect.unreachable();
|
expect.unreachable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
expectCalls(bar, {
|
expectCalls(bar, {
|
||||||
errorCalls: ["Error: Process failed with code 500"],
|
errorCalls: ["Error: Process failed with code 500"],
|
||||||
redraws: 0
|
redraws: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
113
src/Progress.ts
113
src/Progress.ts
|
@ -1,16 +1,20 @@
|
||||||
import chalk from 'chalk';
|
import chalk from "chalk";
|
||||||
import { convertHSVtoRGB, getColor, type CustomLoggerColor } from './internal.ts';
|
import {
|
||||||
import { defaultSpinnerOptions } from './Spinner.ts';
|
convertHSVtoRGB,
|
||||||
import { isUnicodeSupported } from './console.ts';
|
type CustomLoggerColor,
|
||||||
import { Widget } from './Widget.ts';
|
getColor,
|
||||||
|
} from "./internal.ts";
|
||||||
|
import { defaultSpinnerOptions } from "./Spinner.ts";
|
||||||
|
import { isUnicodeSupported } from "./console.ts";
|
||||||
|
import { Widget } from "./Widget.ts";
|
||||||
|
|
||||||
const boxChars = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
const boxChars = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
|
||||||
const fullBox = '█';
|
const fullBox = "█";
|
||||||
const asciiChars = {
|
const asciiChars = {
|
||||||
start: '[',
|
start: "[",
|
||||||
end: ']',
|
end: "]",
|
||||||
empty: ' ',
|
empty: " ",
|
||||||
fill: '=',
|
fill: "=",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,7 +30,7 @@ function getUnicodeBar(progress: number, width: number) {
|
||||||
return fullBox.repeat(width);
|
return fullBox.repeat(width);
|
||||||
}
|
}
|
||||||
if (progress <= 0 || isNaN(progress)) {
|
if (progress <= 0 || isNaN(progress)) {
|
||||||
return ' '.repeat(width);
|
return " ".repeat(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wholeWidth = Math.floor(progress * width);
|
const wholeWidth = Math.floor(progress * width);
|
||||||
|
@ -34,11 +38,11 @@ function getUnicodeBar(progress: number, width: number) {
|
||||||
const partWidth = Math.floor(remainderWidth * 8);
|
const partWidth = Math.floor(remainderWidth * 8);
|
||||||
let partChar = boxChars[partWidth];
|
let partChar = boxChars[partWidth];
|
||||||
if (width - wholeWidth - 1 < 0) {
|
if (width - wholeWidth - 1 < 0) {
|
||||||
partChar = '';
|
partChar = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const fill = fullBox.repeat(wholeWidth);
|
const fill = fullBox.repeat(wholeWidth);
|
||||||
const empty = ' '.repeat(width - wholeWidth - 1);
|
const empty = " ".repeat(width - wholeWidth - 1);
|
||||||
|
|
||||||
return `${fill}${partChar}${empty}`;
|
return `${fill}${partChar}${empty}`;
|
||||||
}
|
}
|
||||||
|
@ -50,14 +54,16 @@ function getAsciiBar(progress: number, width: number) {
|
||||||
asciiChars.fill.repeat(Math.floor(progress * (width - 2))),
|
asciiChars.fill.repeat(Math.floor(progress * (width - 2))),
|
||||||
asciiChars.empty.repeat(width - Math.ceil(progress * (width - 2))),
|
asciiChars.empty.repeat(width - Math.ceil(progress * (width - 2))),
|
||||||
asciiChars.end,
|
asciiChars.end,
|
||||||
].join('');
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A Progress Bar Style. Ascii is forced in non-unicode terminals. */
|
/** A Progress Bar Style. Ascii is forced in non-unicode terminals. */
|
||||||
export type BarStyle = 'unicode' | 'ascii';
|
export type BarStyle = "unicode" | "ascii";
|
||||||
|
|
||||||
/** Options to be passed to `new Progress` */
|
/** Options to be passed to `new Progress` */
|
||||||
export interface ProgressOptions<Props extends Record<string, unknown> = Record<never, unknown>> {
|
export interface ProgressOptions<
|
||||||
|
Props extends Record<string, unknown> = Record<never, unknown>,
|
||||||
|
> {
|
||||||
/** Text displayed to the right of the bar. */
|
/** Text displayed to the right of the bar. */
|
||||||
text: string | ((props: ExtendedProps<Props>) => string);
|
text: string | ((props: ExtendedProps<Props>) => string);
|
||||||
/** Text displayed to the left of the bar, if specified. */
|
/** Text displayed to the left of the bar, if specified. */
|
||||||
|
@ -82,17 +88,17 @@ export interface BarSpinnerOptions {
|
||||||
/** Sequence of frames for the spinner. */
|
/** Sequence of frames for the spinner. */
|
||||||
frames: string[];
|
frames: string[];
|
||||||
/** Color of the spinner. If set to `match` it will match the bar. */
|
/** Color of the spinner. If set to `match` it will match the bar. */
|
||||||
color: CustomLoggerColor | 'match';
|
color: CustomLoggerColor | "match";
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
beforeText: '',
|
beforeText: "",
|
||||||
barWidth: 35,
|
barWidth: 35,
|
||||||
barColor: 'rgb',
|
barColor: "rgb",
|
||||||
barStyle: 'unicode',
|
barStyle: "unicode",
|
||||||
spinner: {
|
spinner: {
|
||||||
...defaultSpinnerOptions,
|
...defaultSpinnerOptions,
|
||||||
color: 'match',
|
color: "match",
|
||||||
},
|
},
|
||||||
value: 0,
|
value: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
|
@ -105,12 +111,14 @@ type ExtendedProps<T> = T & {
|
||||||
progress: number;
|
progress: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Progress<Props extends Record<string, unknown> = Record<never, unknown>> extends Widget {
|
export class Progress<
|
||||||
|
Props extends Record<string, unknown> = Record<never, unknown>,
|
||||||
|
> extends Widget {
|
||||||
#text: string | ((props: ExtendedProps<Props>) => string);
|
#text: string | ((props: ExtendedProps<Props>) => string);
|
||||||
#beforeText: string | ((props: ExtendedProps<Props>) => string);
|
#beforeText: string | ((props: ExtendedProps<Props>) => string);
|
||||||
#barWidth: number;
|
#barWidth: number;
|
||||||
#barStyle: NonNullable<ProgressOptions['barStyle']>;
|
#barStyle: NonNullable<ProgressOptions["barStyle"]>;
|
||||||
#spinnerColor: NonNullable<BarSpinnerOptions['color']>;
|
#spinnerColor: NonNullable<BarSpinnerOptions["color"]>;
|
||||||
#spinnerFrames?: readonly string[];
|
#spinnerFrames?: readonly string[];
|
||||||
#props: Props;
|
#props: Props;
|
||||||
#spinnerFPS: number;
|
#spinnerFPS: number;
|
||||||
|
@ -121,7 +129,7 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
constructor(options: ProgressOptions<Props> | string) {
|
constructor(options: ProgressOptions<Props> | string) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
if (typeof options === 'string') {
|
if (typeof options === "string") {
|
||||||
options = { text: options };
|
options = { text: options };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,8 +146,10 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
if (options.spinner !== null) {
|
if (options.spinner !== null) {
|
||||||
this.fps = 15;
|
this.fps = 15;
|
||||||
this.#spinnerFPS = options.spinner?.fps ?? defaultOptions.spinner.fps;
|
this.#spinnerFPS = options.spinner?.fps ?? defaultOptions.spinner.fps;
|
||||||
this.#spinnerFrames = options.spinner?.frames ?? defaultOptions.spinner.frames;
|
this.#spinnerFrames = options.spinner?.frames ??
|
||||||
this.#spinnerColor = options.spinner?.color ?? defaultOptions.spinner.color;
|
defaultOptions.spinner.frames;
|
||||||
|
this.#spinnerColor = options.spinner?.color ??
|
||||||
|
defaultOptions.spinner.color;
|
||||||
} else {
|
} else {
|
||||||
this.fps = 0;
|
this.fps = 0;
|
||||||
this.#spinnerFPS = defaultOptions.spinner.fps;
|
this.#spinnerFPS = defaultOptions.spinner.fps;
|
||||||
|
@ -168,7 +178,9 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
|
|
||||||
/** Text displayed to the right of the bar. */
|
/** Text displayed to the right of the bar. */
|
||||||
get text(): string {
|
get text(): string {
|
||||||
return typeof this.#text === 'function' ? this.#text(this.props) : this.#text;
|
return typeof this.#text === "function"
|
||||||
|
? this.#text(this.props)
|
||||||
|
: this.#text;
|
||||||
}
|
}
|
||||||
|
|
||||||
set text(value: string | (() => string)) {
|
set text(value: string | (() => string)) {
|
||||||
|
@ -178,7 +190,9 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
|
|
||||||
/** Text displayed to the left of the bar, if specified. */
|
/** Text displayed to the left of the bar, if specified. */
|
||||||
get beforeText(): string {
|
get beforeText(): string {
|
||||||
return typeof this.#beforeText === 'function' ? this.#beforeText(this.props) : this.#beforeText;
|
return typeof this.#beforeText === "function"
|
||||||
|
? this.#beforeText(this.props)
|
||||||
|
: this.#beforeText;
|
||||||
}
|
}
|
||||||
|
|
||||||
set beforeText(value: string | (() => string)) {
|
set beforeText(value: string | (() => string)) {
|
||||||
|
@ -186,7 +200,7 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
this.redraw();
|
this.redraw();
|
||||||
}
|
}
|
||||||
/** Current value of progress bar. */
|
/** Current value of progress bar. */
|
||||||
get value() {
|
get value(): number {
|
||||||
return this.#value;
|
return this.#value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +210,7 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Total value of progress bar. When value === total, the bar is full. */
|
/** Total value of progress bar. When value === total, the bar is full. */
|
||||||
get total() {
|
get total(): number {
|
||||||
return this.#total;
|
return this.#total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,34 +241,37 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
|
|
||||||
let spinner;
|
let spinner;
|
||||||
if (this.#spinnerFrames) {
|
if (this.#spinnerFrames) {
|
||||||
const frame = Math.floor(now / (1000 / this.#spinnerFPS)) % this.#spinnerFrames.length;
|
const frame = Math.floor(now / (1000 / this.#spinnerFPS)) %
|
||||||
|
this.#spinnerFrames.length;
|
||||||
spinner = this.#spinnerColor
|
spinner = this.#spinnerColor
|
||||||
? (this.#spinnerColor === 'match'
|
? (this.#spinnerColor === "match"
|
||||||
? chalk.rgb(...convertHSVtoRGB(hue, 0.8, 1))
|
? chalk.rgb(...convertHSVtoRGB(hue, 0.8, 1))
|
||||||
: getColor(this.#spinnerColor))(this.#spinnerFrames[frame])
|
: getColor(this.#spinnerColor))(this.#spinnerFrames[frame])
|
||||||
: this.#spinnerFrames[frame];
|
: this.#spinnerFrames[frame];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBar = isUnicodeSupported && this.#barStyle === 'unicode' ? getUnicodeBar : getAsciiBar;
|
const getBar = isUnicodeSupported && this.#barStyle === "unicode"
|
||||||
|
? getUnicodeBar
|
||||||
|
: getAsciiBar;
|
||||||
|
|
||||||
const beforeText = this.beforeText;
|
const beforeText = this.beforeText;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
spinner ? spinner + ' ' : '',
|
spinner ? spinner + " " : "",
|
||||||
beforeText ? beforeText + ' ' : '',
|
beforeText ? beforeText + " " : "",
|
||||||
barColor(getBar(progress, this.#barWidth)),
|
barColor(getBar(progress, this.#barWidth)),
|
||||||
' ',
|
" ",
|
||||||
this.text,
|
this.text,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('');
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
success(message?: string): void {
|
override success(message?: string): void {
|
||||||
super.success(message ?? this.text);
|
super.success(message ?? this.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message?: string | Error): void {
|
override error(message?: string | Error): void {
|
||||||
super.error(message ?? this.text);
|
super.error(message ?? this.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,7 +287,7 @@ export interface WithProgressOptions<Props extends Record<string, unknown>, T>
|
||||||
/** Calls a function with a progress bar. */
|
/** Calls a function with a progress bar. */
|
||||||
export async function withProgress<Props extends Record<string, unknown>, T>(
|
export async function withProgress<Props extends Record<string, unknown>, T>(
|
||||||
opts: WithProgressOptions<Props, T> | string,
|
opts: WithProgressOptions<Props, T> | string,
|
||||||
fn: (bar: Progress<Props>) => Promise<T>
|
fn: (bar: Progress<Props>) => Promise<T>,
|
||||||
): Promise<T>;
|
): Promise<T>;
|
||||||
export async function withProgress(opts: any, fn: any) {
|
export async function withProgress(opts: any, fn: any) {
|
||||||
const bar = new Progress(opts);
|
const bar = new Progress(opts);
|
||||||
|
@ -279,18 +296,18 @@ export async function withProgress(opts: any, fn: any) {
|
||||||
const result = await fn(bar);
|
const result = await fn(bar);
|
||||||
bar.success(
|
bar.success(
|
||||||
opts.successText
|
opts.successText
|
||||||
? typeof opts.successText === 'function'
|
? typeof opts.successText === "function"
|
||||||
? opts.successText(result)
|
? opts.successText(result)
|
||||||
: opts.successText
|
: opts.successText
|
||||||
: opts.text
|
: opts.text
|
||||||
? typeof opts.text === 'function'
|
? typeof opts.text === "function" ? opts.text(bar.props) : opts.text
|
||||||
? opts.text(bar.props)
|
: "Completed",
|
||||||
: opts.text
|
|
||||||
: 'Completed'
|
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
bar.error(
|
bar.error(
|
||||||
typeof opts.failureText === 'function' ? opts.failureText(error) : opts.failureText ?? error
|
typeof opts.failureText === "function"
|
||||||
|
? opts.failureText(error)
|
||||||
|
: (opts.failureText ?? error),
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,288 +1,299 @@
|
||||||
import { beforeEach, expect, test } from "bun:test";
|
import { beforeEach, expect, test } from "bun:test";
|
||||||
import { reset, expectCalls, getWidgets } from "./test/widget-mock.ts";
|
import { expectCalls, getWidgets, reset } from "./test/widget-mock.ts";
|
||||||
|
|
||||||
const { Spinner, defaultSpinnerOptions, withSpinner } = await import("@paperclover/console/Spinner");
|
const { Spinner, defaultSpinnerOptions, withSpinner } = await import(
|
||||||
type Spinner<T extends Record<string, unknown> = Record<string, unknown>> = import("@paperclover/console/Spinner").Spinner<T>;
|
"@paperclover/console/Spinner"
|
||||||
|
);
|
||||||
|
type Spinner<T extends Record<string, unknown> = Record<string, unknown>> =
|
||||||
|
import("@paperclover/console/Spinner").Spinner<T>;
|
||||||
|
|
||||||
beforeEach(reset);
|
beforeEach(reset);
|
||||||
|
|
||||||
test("default options and rendering", () => {
|
test("default options and rendering", () => {
|
||||||
const spinner = new Spinner("spin");
|
const spinner = new Spinner("spin");
|
||||||
expect(spinner.props).toEqual({});
|
expect(spinner.props).toEqual({});
|
||||||
expect(spinner.fps).toEqual(defaultSpinnerOptions.fps);
|
expect(spinner.fps).toEqual(defaultSpinnerOptions.fps);
|
||||||
expect(spinner.frames).toEqual(defaultSpinnerOptions.frames);
|
expect(spinner.frames).toEqual(defaultSpinnerOptions.frames);
|
||||||
expect(defaultSpinnerOptions.frames).toHaveLength(10);
|
expect(defaultSpinnerOptions.frames).toHaveLength(10);
|
||||||
|
|
||||||
let now = 0;
|
let now = 0;
|
||||||
for (let i = 0; i < spinner.frames.length; i++) {
|
for (let i = 0; i < spinner.frames.length; i++) {
|
||||||
expect(spinner.format(now)).toEqual(`\x1b[94m${spinner.frames[i]}\x1b[39m spin`);
|
expect(spinner.format(now)).toEqual(
|
||||||
now += 1000 / spinner.fps;
|
`\x1b[94m${spinner.frames[i]}\x1b[39m spin`,
|
||||||
}
|
);
|
||||||
|
now += 1000 / spinner.fps;
|
||||||
|
}
|
||||||
|
|
||||||
spinner.text = "something";
|
spinner.text = "something";
|
||||||
|
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
redraws: 1,
|
redraws: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("custom options apply correctly", () => {
|
test("custom options apply correctly", () => {
|
||||||
const customFrames = ['A', 'B', 'C'];
|
const customFrames = ["A", "B", "C"];
|
||||||
const customFps = 24;
|
const customFps = 24;
|
||||||
|
|
||||||
const spinner = new Spinner({
|
const spinner = new Spinner({
|
||||||
text: "custom spinner",
|
text: "custom spinner",
|
||||||
frames: customFrames,
|
frames: customFrames,
|
||||||
fps: customFps
|
fps: customFps,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spinner.frames).toEqual(customFrames);
|
expect(spinner.frames).toEqual(customFrames);
|
||||||
expect(spinner.fps).toEqual(customFps);
|
expect(spinner.fps).toEqual(customFps);
|
||||||
expect(spinner.text).toEqual("custom spinner");
|
expect(spinner.text).toEqual("custom spinner");
|
||||||
|
|
||||||
let now = 0;
|
let now = 0;
|
||||||
for (let i = 0; i < spinner.frames.length; i++) {
|
for (let i = 0; i < spinner.frames.length; i++) {
|
||||||
expect(spinner.format(now)).toEqual(`\x1b[94m${spinner.frames[i]}\x1b[39m custom spinner`);
|
expect(spinner.format(now)).toEqual(
|
||||||
now += 1000 / spinner.fps;
|
`\x1b[94m${spinner.frames[i]}\x1b[39m custom spinner`,
|
||||||
}
|
);
|
||||||
|
now += 1000 / spinner.fps;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("custom color options", () => {
|
test("custom color options", () => {
|
||||||
// Test with named color
|
// Test with named color
|
||||||
const redSpinner = new Spinner({
|
const redSpinner = new Spinner({
|
||||||
text: "red spinner",
|
text: "red spinner",
|
||||||
color: "red"
|
color: "red",
|
||||||
});
|
});
|
||||||
expect(redSpinner.format(0)).toEqual(`\x1b[31m${redSpinner.frames[0]}\x1b[39m red spinner`);
|
expect(redSpinner.format(0)).toEqual(
|
||||||
|
`\x1b[31m${redSpinner.frames[0]}\x1b[39m red spinner`,
|
||||||
|
);
|
||||||
|
|
||||||
// Test with hex color
|
// Test with hex color
|
||||||
const hexSpinner = new Spinner({
|
const hexSpinner = new Spinner({
|
||||||
text: "hex spinner",
|
text: "hex spinner",
|
||||||
color: "#ff00ff"
|
color: "#ff00ff",
|
||||||
});
|
});
|
||||||
expect(hexSpinner.format(0)).toContain(hexSpinner.frames[0]);
|
expect(hexSpinner.format(0)).toContain(hexSpinner.frames[0]);
|
||||||
expect(hexSpinner.format(0)).toContain("hex spinner");
|
expect(hexSpinner.format(0)).toContain("hex spinner");
|
||||||
|
|
||||||
// Test with rgb array
|
// Test with rgb array
|
||||||
const rgbSpinner = new Spinner({
|
const rgbSpinner = new Spinner({
|
||||||
text: "rgb spinner",
|
text: "rgb spinner",
|
||||||
color: [255, 0, 255]
|
color: [255, 0, 255],
|
||||||
});
|
});
|
||||||
expect(rgbSpinner.format(0)).toContain(rgbSpinner.frames[0]);
|
expect(rgbSpinner.format(0)).toContain(rgbSpinner.frames[0]);
|
||||||
expect(rgbSpinner.format(0)).toContain("rgb spinner");
|
expect(rgbSpinner.format(0)).toContain("rgb spinner");
|
||||||
|
|
||||||
// Test with color disabled
|
// Test with color disabled
|
||||||
const noColorSpinner = new Spinner({
|
const noColorSpinner = new Spinner({
|
||||||
text: "no color",
|
text: "no color",
|
||||||
color: false
|
color: false,
|
||||||
});
|
});
|
||||||
expect(noColorSpinner.format(0)).toEqual(`${noColorSpinner.frames[0]} no color`);
|
expect(noColorSpinner.format(0)).toEqual(
|
||||||
|
`${noColorSpinner.frames[0]} no color`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("invalid color throws error", () => {
|
test("invalid color throws error", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new Spinner({
|
new Spinner({
|
||||||
text: "invalid color",
|
text: "invalid color",
|
||||||
color: "invalidColor" as any
|
color: "invalidColor" as any,
|
||||||
});
|
});
|
||||||
}).toThrow("Invalid color: invalidColor");
|
}).toThrow("Invalid color: invalidColor");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("text getter and setter", () => {
|
test("text getter and setter", () => {
|
||||||
const spinner = new Spinner("initial text");
|
const spinner = new Spinner("initial text");
|
||||||
expect(spinner.text).toEqual("initial text");
|
expect(spinner.text).toEqual("initial text");
|
||||||
|
|
||||||
spinner.text = "updated text";
|
spinner.text = "updated text";
|
||||||
expect(spinner.text).toEqual("updated text");
|
expect(spinner.text).toEqual("updated text");
|
||||||
|
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("function for text field and props getter and setter", () => {
|
test("function for text field and props getter and setter", () => {
|
||||||
const textFn = (props: { count: number }) => `Items: ${props.count}`;
|
const textFn = (props: { count: number }) => `Items: ${props.count}`;
|
||||||
const spinner = new Spinner<{ count: number; newProp?: string }>({
|
const spinner = new Spinner<{ count: number; newProp?: string }>({
|
||||||
text: textFn,
|
text: textFn,
|
||||||
props: { count: 5 }
|
props: { count: 5 },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spinner.text).toEqual("Items: 5");
|
expect(spinner.text).toEqual("Items: 5");
|
||||||
expect(spinner.props).toEqual({ count: 5 });
|
expect(spinner.props).toEqual({ count: 5 });
|
||||||
|
|
||||||
spinner.props = { count: 10 };
|
spinner.props = { count: 10 };
|
||||||
expect(spinner.text).toEqual("Items: 10");
|
expect(spinner.text).toEqual("Items: 10");
|
||||||
expect(spinner.props).toEqual({ count: 10 });
|
expect(spinner.props).toEqual({ count: 10 });
|
||||||
|
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adding new props should keep existing ones
|
// Adding new props should keep existing ones
|
||||||
spinner.props = { newProp: "value" };
|
spinner.props = { newProp: "value" };
|
||||||
expect(spinner.props).toEqual({ count: 10, newProp: "value" });
|
expect(spinner.props).toEqual({ count: 10, newProp: "value" });
|
||||||
|
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
redraws: 2
|
redraws: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update method with string", () => {
|
test("update method with string", () => {
|
||||||
const spinner = new Spinner("initial");
|
const spinner = new Spinner("initial");
|
||||||
|
|
||||||
spinner.update("updated via update");
|
spinner.update("updated via update");
|
||||||
expect(spinner.text).toEqual("updated via update");
|
expect(spinner.text).toEqual("updated via update");
|
||||||
|
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
redraws: 1
|
redraws: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update method with props", () => {
|
test("update method with props", () => {
|
||||||
const textFn = (props: { count: number, name?: string }) =>
|
const textFn = (props: { count: number; name?: string }) =>
|
||||||
`${props.name || 'Items'}: ${props.count}`;
|
`${props.name || "Items"}: ${props.count}`;
|
||||||
|
|
||||||
const spinner = new Spinner<{ count: number, name?: string }>({
|
const spinner = new Spinner<{ count: number; name?: string }>({
|
||||||
text: textFn,
|
text: textFn,
|
||||||
props: { count: 0 }
|
props: { count: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spinner.text).toEqual("Items: 0");
|
expect(spinner.text).toEqual("Items: 0");
|
||||||
|
|
||||||
spinner.update({ count: 5 });
|
spinner.update({ count: 5 });
|
||||||
expect(spinner.text).toEqual("Items: 5");
|
expect(spinner.text).toEqual("Items: 5");
|
||||||
|
|
||||||
spinner.update({ name: "Products" });
|
spinner.update({ name: "Products" });
|
||||||
expect(spinner.text).toEqual("Products: 5");
|
expect(spinner.text).toEqual("Products: 5");
|
||||||
|
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
redraws: 2
|
redraws: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("success method", () => {
|
test("success method", () => {
|
||||||
const spinner = new Spinner("working");
|
const spinner = new Spinner("working");
|
||||||
|
|
||||||
spinner.success();
|
spinner.success();
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
successCalls: ["working"]
|
successCalls: ["working"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const spinner2 = new Spinner("working");
|
const spinner2 = new Spinner("working");
|
||||||
spinner2.success("completed");
|
spinner2.success("completed");
|
||||||
expectCalls(spinner2, {
|
expectCalls(spinner2, {
|
||||||
successCalls: ["completed"]
|
successCalls: ["completed"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("error method", () => {
|
test("error method", () => {
|
||||||
const spinner = new Spinner("working");
|
const spinner = new Spinner("working");
|
||||||
|
|
||||||
spinner.error();
|
spinner.error();
|
||||||
expectCalls(spinner, {
|
expectCalls(spinner, {
|
||||||
errorCalls: ["working"]
|
errorCalls: ["working"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const spinner2 = new Spinner("working");
|
const spinner2 = new Spinner("working");
|
||||||
spinner2.error("failed");
|
spinner2.error("failed");
|
||||||
expectCalls(spinner2, {
|
expectCalls(spinner2, {
|
||||||
errorCalls: ["failed"]
|
errorCalls: ["failed"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const error = new Error("Something went wrong");
|
const error = new Error("Something went wrong");
|
||||||
const spinner3 = new Spinner("working");
|
const spinner3 = new Spinner("working");
|
||||||
spinner3.error(error);
|
spinner3.error(error);
|
||||||
expectCalls(spinner3, {
|
expectCalls(spinner3, {
|
||||||
errorCalls: [error]
|
errorCalls: [error],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withSpinner function", async () => {
|
test("withSpinner function", async () => {
|
||||||
const expectedResult = Symbol("unique");
|
const expectedResult = Symbol("unique");
|
||||||
let spin!: Spinner;
|
let spin!: Spinner;
|
||||||
|
|
||||||
const result = await withSpinner("Loading data", async (spinner) => {
|
const result = await withSpinner("Loading data", async (spinner) => {
|
||||||
spin = spinner;
|
spin = spinner;
|
||||||
expect(spinner.text).toEqual("Loading data");
|
expect(spinner.text).toEqual("Loading data");
|
||||||
return expectedResult;
|
return expectedResult;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
expect(result).toEqual(expectedResult);
|
||||||
expectCalls(spin, {
|
expectCalls(spin, {
|
||||||
successCalls: ["Completed"],
|
successCalls: ["Completed"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withSpinner function with custom success text", async () => {
|
test("withSpinner function with custom success text", async () => {
|
||||||
let spin!: Spinner;
|
let spin!: Spinner;
|
||||||
const result = await withSpinner({
|
const result = await withSpinner({
|
||||||
text: "Loading",
|
text: "Loading",
|
||||||
successText: "Data loaded successfully"
|
successText: "Data loaded successfully",
|
||||||
}, async (spinner) => {
|
}, async (spinner) => {
|
||||||
spin = spinner;
|
spin = spinner;
|
||||||
return "data";
|
return "data";
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual("data");
|
expect(result).toEqual("data");
|
||||||
expectCalls(spin, {
|
expectCalls(spin, {
|
||||||
successCalls: ["Data loaded successfully"],
|
successCalls: ["Data loaded successfully"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withSpinner function with success text function", async () => {
|
test("withSpinner function with success text function", async () => {
|
||||||
let spin!: Spinner;
|
let spin!: Spinner;
|
||||||
const result = await withSpinner({
|
const result = await withSpinner({
|
||||||
text: "Loading",
|
text: "Loading",
|
||||||
successText: (data: number[]) => `Loaded ${data.length} items`
|
successText: (data: number[]) => `Loaded ${data.length} items`,
|
||||||
}, async (spinner) => {
|
}, async (spinner) => {
|
||||||
spin = spinner;
|
spin = spinner;
|
||||||
return [1, 2, 3];
|
return [1, 2, 3];
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual([1, 2, 3]);
|
expect(result).toEqual([1, 2, 3]);
|
||||||
expectCalls(spin, {
|
expectCalls(spin, {
|
||||||
successCalls: ["Loaded 3 items"],
|
successCalls: ["Loaded 3 items"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withSpinner function with error", async () => {
|
test("withSpinner function with error", async () => {
|
||||||
const error = new Error("Failed to load data");
|
const error = new Error("Failed to load data");
|
||||||
let spin!: Spinner;
|
let spin!: Spinner;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withSpinner({
|
await withSpinner({
|
||||||
text: "Loading",
|
text: "Loading",
|
||||||
failureText: "Could not load data"
|
failureText: "Could not load data",
|
||||||
}, async (spinner) => {
|
}, async (spinner) => {
|
||||||
spin = spinner;
|
spin = spinner;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
expect.unreachable();
|
expect.unreachable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
expectCalls(spin, {
|
expectCalls(spin, {
|
||||||
errorCalls: ["Could not load data"]
|
errorCalls: ["Could not load data"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withSpinner function with error text function", async () => {
|
test("withSpinner function with error text function", async () => {
|
||||||
const error = new Error("Network error");
|
const error = new Error("Network error");
|
||||||
let spin!: Spinner;
|
let spin!: Spinner;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withSpinner({
|
await withSpinner({
|
||||||
text: "Loading",
|
text: "Loading",
|
||||||
failureText: (err) => `Error: ${err.message}`
|
failureText: (err) => `Error: ${err.message}`,
|
||||||
}, async (spinner) => {
|
}, async (spinner) => {
|
||||||
spin = spinner;
|
spin = spinner;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
expect.unreachable();
|
expect.unreachable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
expectCalls(spin, {
|
expectCalls(spin, {
|
||||||
errorCalls: ["Error: Network error"]
|
errorCalls: ["Error: Network error"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Widget } from './Widget.ts';
|
import { Widget } from "./Widget.ts";
|
||||||
import { getColor, type CustomLoggerColor } from './internal.ts';
|
import { type CustomLoggerColor, getColor } from "./internal.ts";
|
||||||
|
|
||||||
export interface SpinnerOptions<Props extends Record<string, unknown>> {
|
export interface SpinnerOptions<Props extends Record<string, unknown>> {
|
||||||
/** Text displayed to the right of the spinner. */
|
/** Text displayed to the right of the spinner. */
|
||||||
|
@ -15,13 +15,15 @@ export interface SpinnerOptions<Props extends Record<string, unknown>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSpinnerOptions = {
|
export const defaultSpinnerOptions = {
|
||||||
text: 'Loading...',
|
text: "Loading...",
|
||||||
color: 'blueBright',
|
color: "blueBright",
|
||||||
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
||||||
fps: 12.5,
|
fps: 12.5,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class Spinner<Props extends Record<string, unknown> = Record<string, unknown>> extends Widget {
|
export class Spinner<
|
||||||
|
Props extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
> extends Widget {
|
||||||
readonly frames: readonly string[];
|
readonly frames: readonly string[];
|
||||||
#text: string | ((props: Props) => string);
|
#text: string | ((props: Props) => string);
|
||||||
#color: ((text: string) => string) | null;
|
#color: ((text: string) => string) | null;
|
||||||
|
@ -30,7 +32,7 @@ export class Spinner<Props extends Record<string, unknown> = Record<string, unkn
|
||||||
|
|
||||||
constructor(options: SpinnerOptions<Props> | string) {
|
constructor(options: SpinnerOptions<Props> | string) {
|
||||||
super();
|
super();
|
||||||
if (typeof options === 'string') {
|
if (typeof options === "string") {
|
||||||
options = { text: options };
|
options = { text: options };
|
||||||
}
|
}
|
||||||
this.#text = options.text ?? defaultSpinnerOptions.text;
|
this.#text = options.text ?? defaultSpinnerOptions.text;
|
||||||
|
@ -43,7 +45,9 @@ export class Spinner<Props extends Record<string, unknown> = Record<string, unkn
|
||||||
|
|
||||||
/** Text displayed to the right of the spinner. */
|
/** Text displayed to the right of the spinner. */
|
||||||
get text(): string {
|
get text(): string {
|
||||||
return typeof this.#text === 'function' ? this.#text(this.#props) : this.#text;
|
return typeof this.#text === "function"
|
||||||
|
? this.#text(this.#props)
|
||||||
|
: this.#text;
|
||||||
}
|
}
|
||||||
|
|
||||||
set text(value: string | (() => string)) {
|
set text(value: string | (() => string)) {
|
||||||
|
@ -73,7 +77,7 @@ export class Spinner<Props extends Record<string, unknown> = Record<string, unkn
|
||||||
update(newProps: Partial<Props>): void;
|
update(newProps: Partial<Props>): void;
|
||||||
update(newMessage: string): void;
|
update(newMessage: string): void;
|
||||||
update(newData: string | Partial<Props>) {
|
update(newData: string | Partial<Props>) {
|
||||||
if (typeof newData === 'string') {
|
if (typeof newData === "string") {
|
||||||
this.text = newData;
|
this.text = newData;
|
||||||
} else {
|
} else {
|
||||||
this.#props = { ...this.#props, ...newData };
|
this.#props = { ...this.#props, ...newData };
|
||||||
|
@ -84,18 +88,14 @@ export class Spinner<Props extends Record<string, unknown> = Record<string, unkn
|
||||||
format(now: number): string {
|
format(now: number): string {
|
||||||
const frame = Math.floor(now / (1000 / this.fps)) % this.frames.length;
|
const frame = Math.floor(now / (1000 / this.fps)) % this.frames.length;
|
||||||
const frameText = this.frames[frame]!;
|
const frameText = this.frames[frame]!;
|
||||||
return (
|
return (this.#color ? this.#color(frameText) : frameText) + " " + this.text;
|
||||||
(this.#color ? this.#color(frameText) : frameText) +
|
|
||||||
' ' +
|
|
||||||
this.text
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
success(message?: string): void {
|
override success(message?: string): void {
|
||||||
super.success(message ?? this.text);
|
super.success(message ?? this.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message?: string | Error): void {
|
override error(message?: string | Error): void {
|
||||||
super.error(message ?? this.text);
|
super.error(message ?? this.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ export interface WithSpinnerOptions<Props extends Record<string, unknown>, T>
|
||||||
/** Calls a function with a spinner. */
|
/** Calls a function with a spinner. */
|
||||||
export async function withSpinner<Props extends Record<string, unknown>, T>(
|
export async function withSpinner<Props extends Record<string, unknown>, T>(
|
||||||
spinnerOptions: WithSpinnerOptions<Props, T> | string,
|
spinnerOptions: WithSpinnerOptions<Props, T> | string,
|
||||||
fn: (spinner: Spinner<Props>) => Promise<T>
|
fn: (spinner: Spinner<Props>) => Promise<T>,
|
||||||
): Promise<T>;
|
): Promise<T>;
|
||||||
export async function withSpinner(opts: any, fn: any) {
|
export async function withSpinner(opts: any, fn: any) {
|
||||||
const spinner = new Spinner(opts);
|
const spinner = new Spinner(opts);
|
||||||
|
@ -119,20 +119,22 @@ export async function withSpinner(opts: any, fn: any) {
|
||||||
if (spinner.active) {
|
if (spinner.active) {
|
||||||
spinner.success(
|
spinner.success(
|
||||||
opts.successText
|
opts.successText
|
||||||
? typeof opts.successText === 'function'
|
? typeof opts.successText === "function"
|
||||||
? opts.successText(result)
|
? opts.successText(result)
|
||||||
: opts.successText
|
: opts.successText
|
||||||
: opts.text
|
: opts.text
|
||||||
? typeof opts.text === 'function'
|
? typeof opts.text === "function"
|
||||||
? opts.text(spinner.props)
|
? opts.text(spinner.props)
|
||||||
: opts.text
|
: opts.text
|
||||||
: 'Completed'
|
: "Completed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
spinner.error(
|
spinner.error(
|
||||||
typeof opts.failureText === 'function' ? opts.failureText(error) : opts.failureText ?? error
|
typeof opts.failureText === "function"
|
||||||
|
? opts.failureText(error)
|
||||||
|
: (opts.failureText ?? error),
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import ansi from 'ansi-escapes';
|
import ansi from "ansi-escapes";
|
||||||
import { error, success } from './console.ts';
|
import { error, success } from "./console.ts";
|
||||||
import { flushStderr, writeToStderr } from './internal.ts';
|
import { flushStderr, writeToStderr } from "./internal.ts";
|
||||||
|
|
||||||
|
type Timer = ReturnType<typeof setInterval> & { unref?: () => void };
|
||||||
|
|
||||||
const widgets: Widget[] = [];
|
const widgets: Widget[] = [];
|
||||||
let widgetLineCount = 0;
|
let widgetLineCount = 0;
|
||||||
let widgetTimer: Timer | undefined;
|
let widgetTimer: Timer | undefined;
|
||||||
let redrawingThisTick = false;
|
let redrawingThisTick = false;
|
||||||
|
|
||||||
const kInternalUpdate = Symbol('internalUpdate');
|
const kInternalUpdate = Symbol("internalUpdate");
|
||||||
const kInternalGetText = Symbol('internalGetText');
|
const kInternalGetText = Symbol("internalGetText");
|
||||||
|
|
||||||
function onExit() {
|
function onExit() {
|
||||||
errorAllWidgets('widget alive while process exiting');
|
errorAllWidgets("widget alive while process exiting");
|
||||||
writeToStderr(ansi.cursorShow);
|
writeToStderr(ansi.cursorShow);
|
||||||
flushStderr();
|
flushStderr();
|
||||||
}
|
}
|
||||||
|
@ -29,7 +31,7 @@ export abstract class Widget {
|
||||||
writeToStderr(ansi.cursorHide);
|
writeToStderr(ansi.cursorHide);
|
||||||
widgetTimer = setInterval(redrawWidgetsNoWait, 1000 / 60);
|
widgetTimer = setInterval(redrawWidgetsNoWait, 1000 / 60);
|
||||||
widgetTimer?.unref?.();
|
widgetTimer?.unref?.();
|
||||||
process.on('exit', onExit);
|
process.on("exit", onExit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,11 +56,11 @@ export abstract class Widget {
|
||||||
widgets.splice(index, 1);
|
widgets.splice(index, 1);
|
||||||
redrawWidgetsSoon();
|
redrawWidgetsSoon();
|
||||||
if (finalMessage) {
|
if (finalMessage) {
|
||||||
writeToStderr(finalMessage + '\n');
|
writeToStderr(finalMessage + "\n");
|
||||||
}
|
}
|
||||||
if (widgets.length === 0) {
|
if (widgets.length === 0) {
|
||||||
clearInterval(widgetTimer);
|
clearInterval(widgetTimer);
|
||||||
process.removeListener('exit', onExit);
|
process.removeListener("exit", onExit);
|
||||||
widgetTimer = undefined;
|
widgetTimer = undefined;
|
||||||
writeToStderr(ansi.cursorShow);
|
writeToStderr(ansi.cursorShow);
|
||||||
redrawWidgetsNoWait();
|
redrawWidgetsNoWait();
|
||||||
|
@ -72,23 +74,23 @@ export abstract class Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
#nextUpdate = 0;
|
#nextUpdate = 0;
|
||||||
#text = '';
|
#text = "";
|
||||||
#newlines = 0;
|
#newlines = 0;
|
||||||
|
|
||||||
[kInternalUpdate](now: number) {
|
[kInternalUpdate](now: number): boolean {
|
||||||
if (now > this.#nextUpdate) {
|
if (now > this.#nextUpdate) {
|
||||||
this.#nextUpdate = this.fps === 0 ? Infinity : now + 1000 / this.fps;
|
this.#nextUpdate = this.fps === 0 ? Infinity : now + 1000 / this.fps;
|
||||||
const text = this.format(now);
|
const text = this.format(now);
|
||||||
if (text !== this.#text) {
|
if (text !== this.#text) {
|
||||||
this.#text = text + '\n';
|
this.#text = text + "\n";
|
||||||
this.#newlines = text.split('\n').length;
|
this.#newlines = text.split("\n").length;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[kInternalGetText]() {
|
[kInternalGetText](): string {
|
||||||
widgetLineCount += this.#newlines;
|
widgetLineCount += this.#newlines;
|
||||||
return this.#text;
|
return this.#text;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +107,7 @@ export abstract class Widget {
|
||||||
this.stop();
|
this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
get active() {
|
get active(): boolean {
|
||||||
return widgets.includes(this);
|
return widgets.includes(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,8 +115,10 @@ export abstract class Widget {
|
||||||
export function redrawWidgetsSoon() {
|
export function redrawWidgetsSoon() {
|
||||||
if (widgetLineCount) {
|
if (widgetLineCount) {
|
||||||
writeToStderr(
|
writeToStderr(
|
||||||
'\u001B[?2026h' +
|
"\u001B[?2026h" +
|
||||||
ansi.eraseLine + (ansi.cursorUp(1) + ansi.eraseLine).repeat(widgetLineCount) + '\r',
|
ansi.eraseLine +
|
||||||
|
(ansi.cursorUp(1) + ansi.eraseLine).repeat(widgetLineCount) +
|
||||||
|
"\r",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
widgetLineCount = 0;
|
widgetLineCount = 0;
|
||||||
|
@ -126,18 +130,19 @@ export function redrawWidgetsSoon() {
|
||||||
function redrawWidgetsNoWait() {
|
function redrawWidgetsNoWait() {
|
||||||
redrawingThisTick = false;
|
redrawingThisTick = false;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const hasUpdate = widgets.filter(widget => widget[kInternalUpdate](now)).length > 0;
|
const hasUpdate =
|
||||||
|
widgets.filter((widget) => widget[kInternalUpdate](now)).length > 0;
|
||||||
|
|
||||||
if (hasUpdate || widgetLineCount === 0) {
|
if (hasUpdate || widgetLineCount === 0) {
|
||||||
redrawWidgetsSoon();
|
redrawWidgetsSoon();
|
||||||
writeToStderr(widgets.map(widget => widget[kInternalGetText]()).join(''));
|
writeToStderr(widgets.map((widget) => widget[kInternalGetText]()).join(""));
|
||||||
}
|
}
|
||||||
flushStderr();
|
flushStderr();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function errorAllWidgets(reason: string) {
|
export function errorAllWidgets(reason: string) {
|
||||||
for (const w of widgets) {
|
for (const w of widgets) {
|
||||||
if ('text' in w) {
|
if ("text" in w) {
|
||||||
w.error((w as any).text + ` (due to ${reason})`);
|
w.error((w as any).text + ` (due to ${reason})`);
|
||||||
} else {
|
} else {
|
||||||
w.stop();
|
w.stop();
|
||||||
|
@ -146,8 +151,8 @@ export function errorAllWidgets(reason: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Writes raw line of text without a prefix or filtering. */
|
/** Writes raw line of text without a prefix or filtering. */
|
||||||
export function writeLine(message = '') {
|
export function writeLine(message = "") {
|
||||||
redrawWidgetsSoon();
|
redrawWidgetsSoon();
|
||||||
writeToStderr(message + '\n');
|
writeToStderr(message + "\n");
|
||||||
if (!redrawingThisTick) flushStderr();
|
if (!redrawingThisTick) flushStderr();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,17 @@ import chalk from "chalk";
|
||||||
|
|
||||||
beforeEach(reset);
|
beforeEach(reset);
|
||||||
test("built-in log levels", () => {
|
test("built-in log levels", () => {
|
||||||
const console = require("@paperclover/console");
|
const console = require("@paperclover/console");
|
||||||
console.info("log");
|
console.info("log");
|
||||||
expect(getBuffer()).toEqual(`${chalk.blueBright.bold("info")} log\n`);
|
expect(getBuffer()).toEqual(`${chalk.blueBright.bold("info")} log\n`);
|
||||||
reset();
|
reset();
|
||||||
console.warn("log");
|
console.warn("log");
|
||||||
expect(getBuffer()).toEqual(`${chalk.yellowBright.bold("warn")} ${chalk.yellowBright("log")}\n`);
|
expect(getBuffer()).toEqual(
|
||||||
reset();
|
`${chalk.yellowBright.bold("warn")} ${chalk.yellowBright("log")}\n`,
|
||||||
console.error("log");
|
);
|
||||||
// Don't check the exact color formatting as it may vary
|
reset();
|
||||||
expect(getBuffer()).toContain("✖");
|
console.error("log");
|
||||||
expect(getBuffer()).toContain("log");
|
// Don't check the exact color formatting as it may vary
|
||||||
|
expect(getBuffer()).toContain("✖");
|
||||||
|
expect(getBuffer()).toContain("log");
|
||||||
});
|
});
|
||||||
|
|
415
src/console.ts
415
src/console.ts
|
@ -1,71 +1,141 @@
|
||||||
export const isUnicodeSupported =
|
export const isUnicodeSupported: boolean = process.platform === "win32"
|
||||||
process.platform === 'win32'
|
? Boolean(process.env.CI) ||
|
||||||
? Boolean(process.env.CI) ||
|
Boolean(process.env.WT_SESSION) || // Windows Terminal
|
||||||
Boolean(process.env.WT_SESSION) || // Windows Terminal
|
process.env.ConEmuTask === "{cmd::Cmder}" || // ConEmu and cmder
|
||||||
process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
|
process.env.TERM_PROGRAM === "vscode" ||
|
||||||
process.env.TERM_PROGRAM === 'vscode' ||
|
process.env.TERM === "xterm-256color" ||
|
||||||
process.env.TERM === 'xterm-256color' ||
|
process.env.TERM === "alacritty" ||
|
||||||
process.env.TERM === 'alacritty' ||
|
process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"
|
||||||
process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
|
: process.env.TERM !== "linux";
|
||||||
: process.env.TERM !== 'linux';
|
|
||||||
|
|
||||||
export const errorSymbol = isUnicodeSupported ? '✖' : 'x';
|
export const errorSymbol = isUnicodeSupported ? "✖" : "x";
|
||||||
export const successSymbol = isUnicodeSupported ? '✔' : '√';
|
export const successSymbol = isUnicodeSupported ? "✔" : "√";
|
||||||
export const infoSymbol = isUnicodeSupported ? 'ℹ' : 'i';
|
export const infoSymbol = isUnicodeSupported ? "ℹ" : "i";
|
||||||
export const warningSymbol = isUnicodeSupported ? '⚠' : '‼';
|
export const warningSymbol = isUnicodeSupported ? "⚠" : "‼";
|
||||||
|
|
||||||
let filters: string[] = [];
|
let filters: string[] = [];
|
||||||
let filterGeneration = 0;
|
let filterGeneration = 0;
|
||||||
export function setLogFilter(...newFilters: Array<string | string[]>) {
|
export function setLogFilter(...newFilters: Array<string | string[]>) {
|
||||||
filters = newFilters.flat().map(filter => filter.toLowerCase());
|
filters = newFilters.flat().map((filter) => filter.toLowerCase());
|
||||||
filterGeneration++;
|
filterGeneration++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLogVisible(id: string, defaultVisibility = true) {
|
export function isLogVisible(id: string, defaultVisibility = true): boolean {
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
if (filter === '*') {
|
if (filter === "*") {
|
||||||
defaultVisibility = true;
|
defaultVisibility = true;
|
||||||
} else if (filter === '-*') {
|
} else if (filter === "-*") {
|
||||||
defaultVisibility = false;
|
defaultVisibility = false;
|
||||||
} else if (filter === id || id.startsWith(filter + ':')) {
|
} else if (filter === id || id.startsWith(filter + ":")) {
|
||||||
defaultVisibility = true;
|
defaultVisibility = true;
|
||||||
} else if (filter === '-' + id || id.startsWith('-' + filter + ':')) {
|
} else if (filter === "-" + id || id.startsWith("-" + filter + ":")) {
|
||||||
defaultVisibility = false;
|
defaultVisibility = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return defaultVisibility;
|
}
|
||||||
|
return defaultVisibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.DEBUG !== undefined) {
|
if (process.env.DEBUG !== undefined) {
|
||||||
setLogFilter(
|
setLogFilter(
|
||||||
String(process.env.DEBUG)
|
String(process.env.DEBUG)
|
||||||
.split(',')
|
.split(",")
|
||||||
.map(x => x.trim())
|
.map((x) => x.trim())
|
||||||
.map(x => (['1', 'true', 'all'].includes(x.toLowerCase()) ? '*' : x))
|
.map((x) => (["1", "true", "all"].includes(x.toLowerCase()) ? "*" : x)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Taken from https://github.com/debug-js/debug/blob/d1616622e4d404863c5a98443f755b4006e971dc/src/node.js#L35. */
|
/** Taken from https://github.com/debug-js/debug/blob/d1616622e4d404863c5a98443f755b4006e971dc/src/node.js#L35. */
|
||||||
const debugColors = [
|
const debugColors = [
|
||||||
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, 69, 74, 75, 76, 77,
|
20,
|
||||||
78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134, 135, 148, 149, 160, 161, 162, 163, 164,
|
21,
|
||||||
165, 166, 167, 168, 169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201,
|
26,
|
||||||
202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
|
27,
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
38,
|
||||||
|
39,
|
||||||
|
40,
|
||||||
|
41,
|
||||||
|
42,
|
||||||
|
43,
|
||||||
|
44,
|
||||||
|
45,
|
||||||
|
56,
|
||||||
|
57,
|
||||||
|
62,
|
||||||
|
63,
|
||||||
|
68,
|
||||||
|
69,
|
||||||
|
74,
|
||||||
|
75,
|
||||||
|
76,
|
||||||
|
77,
|
||||||
|
78,
|
||||||
|
79,
|
||||||
|
80,
|
||||||
|
81,
|
||||||
|
92,
|
||||||
|
93,
|
||||||
|
98,
|
||||||
|
99,
|
||||||
|
112,
|
||||||
|
113,
|
||||||
|
128,
|
||||||
|
129,
|
||||||
|
134,
|
||||||
|
135,
|
||||||
|
148,
|
||||||
|
149,
|
||||||
|
160,
|
||||||
|
161,
|
||||||
|
162,
|
||||||
|
163,
|
||||||
|
164,
|
||||||
|
165,
|
||||||
|
166,
|
||||||
|
167,
|
||||||
|
168,
|
||||||
|
169,
|
||||||
|
170,
|
||||||
|
171,
|
||||||
|
172,
|
||||||
|
173,
|
||||||
|
178,
|
||||||
|
179,
|
||||||
|
184,
|
||||||
|
185,
|
||||||
|
196,
|
||||||
|
197,
|
||||||
|
198,
|
||||||
|
199,
|
||||||
|
200,
|
||||||
|
201,
|
||||||
|
202,
|
||||||
|
203,
|
||||||
|
204,
|
||||||
|
205,
|
||||||
|
206,
|
||||||
|
207,
|
||||||
|
208,
|
||||||
|
209,
|
||||||
|
214,
|
||||||
|
215,
|
||||||
|
220,
|
||||||
|
221,
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Converts non string objects into a string the way Node.js' console.log does it. */
|
/** Converts non string objects into a string the way Node.js' console.log does it. */
|
||||||
function stringify(...data: any[]) {
|
function stringify(...data: any[]) {
|
||||||
return data
|
return data
|
||||||
.map(obj => {
|
.map((obj) => {
|
||||||
if (typeof obj === 'string') {
|
if (typeof obj === "string") {
|
||||||
return obj;
|
return obj;
|
||||||
} else if (obj instanceof Error) {
|
} else if (obj instanceof Error) {
|
||||||
return formatErrorObj(obj);
|
return formatErrorObj(obj);
|
||||||
}
|
}
|
||||||
return inspect(obj, false, 4, true);
|
return inspect(obj, false, 4, true);
|
||||||
})
|
})
|
||||||
.join(' ');
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,56 +144,55 @@ function stringify(...data: any[]) {
|
||||||
* Taken from https://github.com/debug-js/debug/blob/master/src/common.js.
|
* Taken from https://github.com/debug-js/debug/blob/master/src/common.js.
|
||||||
*/
|
*/
|
||||||
function selectColor(namespace: string) {
|
function selectColor(namespace: string) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
|
|
||||||
for (let i = 0; i < namespace.length; i++) {
|
for (let i = 0; i < namespace.length; i++) {
|
||||||
hash = (hash << 5) - hash + namespace.charCodeAt(i);
|
hash = (hash << 5) - hash + namespace.charCodeAt(i);
|
||||||
hash |= 0; // Convert to 32bit integer
|
hash |= 0; // Convert to 32bit integer
|
||||||
}
|
}
|
||||||
|
|
||||||
return debugColors[Math.abs(hash) % debugColors.length]!;
|
return debugColors[Math.abs(hash) % debugColors.length]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formatImplementation = {
|
const formatImplementation = {
|
||||||
's': (data: StringLike) => String(data),
|
s: (data: StringLike) => String(data),
|
||||||
'd': (data: number) => String(data),
|
d: (data: number) => String(data),
|
||||||
'i': (data: number) => String(Math.floor(data)),
|
i: (data: number) => String(Math.floor(data)),
|
||||||
'f': (data: number) => String(data),
|
f: (data: number) => String(data),
|
||||||
'x': (data: number) => data.toString(16),
|
x: (data: number) => data.toString(16),
|
||||||
'X': (data: number) => data.toString(16).toUpperCase(),
|
X: (data: number) => data.toString(16).toUpperCase(),
|
||||||
'o': (data: any) => JSON.stringify(data),
|
o: (data: any) => JSON.stringify(data),
|
||||||
'O': (data: any) => JSON.stringify(data, null, 2),
|
O: (data: any) => JSON.stringify(data, null, 2),
|
||||||
'c': () => '',
|
c: () => "",
|
||||||
'j': (data: any) => JSON.stringify(data),
|
j: (data: any) => JSON.stringify(data),
|
||||||
};
|
};
|
||||||
|
|
||||||
function format(fmt: any, ...args: any[]) {
|
function format(fmt: any, ...args: any[]) {
|
||||||
if (typeof fmt === 'string') {
|
if (typeof fmt === "string") {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const result = fmt.replace(/%[%sdifoxXcj]/g, match => {
|
const result = fmt.replace(/%[%sdifoxXcj]/g, (match) => {
|
||||||
if (match === '%%') {
|
if (match === "%%") {
|
||||||
return '%';
|
return "%";
|
||||||
}
|
}
|
||||||
const arg = args[index++];
|
const arg = args[index++];
|
||||||
return (formatImplementation as any)[match[1]!](arg);
|
return (formatImplementation as any)[match[1]!](arg);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (index === 0 && args.length > 0) {
|
if (index === 0 && args.length > 0) {
|
||||||
return result + ' ' + stringify(...args);
|
return result + " " + stringify(...args);
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
return stringify(fmt, ...args);
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return stringify(fmt, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogFunction = {
|
/** @internal */
|
||||||
__proto__: Function.prototype,
|
const LogFunction: {} = {
|
||||||
[Symbol.for('nodejs.util.inspect.custom')](depth: number, options: any) {
|
__proto__: Function.prototype,
|
||||||
return options.stylize(`[LogFunction: ${(this as any).name}]`, 'special');
|
[Symbol.for("nodejs.util.inspect.custom")](depth: number, options: any) {
|
||||||
},
|
return options.stylize(`[LogFunction: ${(this as any).name}]`, "special");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,108 +207,124 @@ const LogFunction = {
|
||||||
* - Using chalk or another formatter on the namespace name.
|
* - Using chalk or another formatter on the namespace name.
|
||||||
*/
|
*/
|
||||||
export function scoped(
|
export function scoped(
|
||||||
name: string,
|
name: string,
|
||||||
opts: CustomLoggerOptions | CustomLoggerColor = {}
|
opts: CustomLoggerOptions | CustomLoggerColor = {},
|
||||||
): LogFunction {
|
): LogFunction {
|
||||||
if (typeof opts === 'string' || Array.isArray(opts) || typeof opts === 'number') {
|
if (
|
||||||
opts = { color: opts };
|
typeof opts === "string" ||
|
||||||
|
Array.isArray(opts) ||
|
||||||
|
typeof opts === "number"
|
||||||
|
) {
|
||||||
|
opts = { color: opts };
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
id = name,
|
||||||
|
color = undefined,
|
||||||
|
coloredText = false,
|
||||||
|
boldText = false,
|
||||||
|
debug = false,
|
||||||
|
} = opts;
|
||||||
|
const strippedName = stripAnsi(name);
|
||||||
|
const colorFn = name.includes("\x1b")
|
||||||
|
? chalk
|
||||||
|
: color
|
||||||
|
? getColor(color)
|
||||||
|
: chalk.ansi256(selectColor(name));
|
||||||
|
const coloredName = colorFn.bold(name);
|
||||||
|
const fn = ((fmt: unknown, ...args: any[]) => {
|
||||||
|
if (!fn.visible) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const {
|
|
||||||
id = name,
|
|
||||||
color = undefined,
|
|
||||||
coloredText = false,
|
|
||||||
boldText = false,
|
|
||||||
debug = false,
|
|
||||||
} = opts;
|
|
||||||
const strippedName = stripAnsi(name);
|
|
||||||
const colorFn = name.includes('\x1b')
|
|
||||||
? chalk
|
|
||||||
: color
|
|
||||||
? getColor(color)
|
|
||||||
: chalk.ansi256(selectColor(name));
|
|
||||||
const coloredName = colorFn.bold(name);
|
|
||||||
const fn = ((fmt: unknown, ...args: any[]) => {
|
|
||||||
if (!fn.visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = format(fmt, ...args).replace(/\n/g, '\n ' + ' '.repeat(strippedName.length));
|
const data = format(fmt, ...args).replace(
|
||||||
|
/\n/g,
|
||||||
|
"\n " + " ".repeat(strippedName.length),
|
||||||
|
);
|
||||||
|
|
||||||
if (fmt === undefined && args.length === 0) {
|
if (fmt === undefined && args.length === 0) {
|
||||||
writeLine();
|
writeLine();
|
||||||
} else {
|
} else {
|
||||||
writeLine(
|
writeLine(
|
||||||
coloredName + ' ' + (coloredText ? (boldText ? colorFn.bold(data) : colorFn(data)) : data)
|
coloredName +
|
||||||
);
|
" " +
|
||||||
}
|
(coloredText ? boldText ? colorFn.bold(data) : colorFn(data) : data),
|
||||||
}) as LogFunction;
|
);
|
||||||
Object.setPrototypeOf(fn, LogFunction);
|
}
|
||||||
Object.defineProperty(fn, 'name', { value: id });
|
}) as LogFunction;
|
||||||
let gen = filterGeneration;
|
Object.setPrototypeOf(fn, LogFunction);
|
||||||
let visible = isLogVisible(id, !debug);
|
Object.defineProperty(fn, "name", { value: id });
|
||||||
Object.defineProperty(fn, 'visible', {
|
let gen = filterGeneration;
|
||||||
get: () => {
|
let visible = isLogVisible(id, !debug);
|
||||||
if (gen !== filterGeneration) {
|
Object.defineProperty(fn, "visible", {
|
||||||
gen = filterGeneration;
|
get: () => {
|
||||||
visible = isLogVisible(id, !debug);
|
if (gen !== filterGeneration) {
|
||||||
}
|
gen = filterGeneration;
|
||||||
return visible;
|
visible = isLogVisible(id, !debug);
|
||||||
}
|
}
|
||||||
});
|
return visible;
|
||||||
return fn;
|
},
|
||||||
|
});
|
||||||
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Built in blue "info" logger. */
|
/** Built in blue "info" logger. */
|
||||||
export const info = /* @__PURE__ */ scoped('info', {
|
export const info: LogFunction = /* @__PURE__ */ scoped("info", {
|
||||||
color: 'blueBright',
|
color: "blueBright",
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Built in yellow "warn" logger. */
|
/** Built in yellow "warn" logger. */
|
||||||
export const warn = /* @__PURE__ */ scoped('warn', {
|
export const warn: LogFunction = /* @__PURE__ */ scoped("warn", {
|
||||||
color: 'yellowBright',
|
color: "yellowBright",
|
||||||
coloredText: true,
|
coloredText: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const _trace = /* @__PURE__ */ scoped('trace', {
|
const _trace: LogFunction = /* @__PURE__ */ scoped("trace", {
|
||||||
color: 208,
|
color: 208,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Built in orange "trace" logger. Prints a stack trace after the message. */
|
/** Built in orange "trace" logger. Prints a stack trace after the message. */
|
||||||
export const trace = /* @__PURE__ */ ((trace: any) => (Object.defineProperty(trace, 'visible', { get: () => _trace.visible }), trace))(function trace(...data: any[]) {
|
export const trace: LogFunction = /* @__PURE__ */ ((trace: any) => (
|
||||||
if (_trace.visible) {
|
Object.defineProperty(trace, "visible", { get: () => _trace.visible }), trace
|
||||||
_trace(...(data.length === 0 ? [' '] : data));
|
))(function trace(...data: any[]) {
|
||||||
writeLine(formatStackTrace(new Error()).split('\n').slice(1).join('\n'));
|
if (_trace.visible) {
|
||||||
}
|
_trace(...(data.length === 0 ? [" "] : data));
|
||||||
|
writeLine(formatStackTrace(new Error()).split("\n").slice(1).join("\n"));
|
||||||
|
}
|
||||||
}) as typeof _trace;
|
}) as typeof _trace;
|
||||||
|
|
||||||
/** Built in red "error" logger, uses a unicode X instead of the word Error. */
|
/** Built in red "error" logger, uses a unicode X instead of the word Error. */
|
||||||
export const error = /* @__PURE__ */ scoped(errorSymbol, {
|
export const error: LogFunction = /* @__PURE__ */ scoped(errorSymbol, {
|
||||||
id: 'error',
|
id: "error",
|
||||||
color: 'redBright',
|
color: "redBright",
|
||||||
coloredText: true,
|
coloredText: true,
|
||||||
boldText: true,
|
boldText: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Built in cyan "debug" logger. */
|
/** Built in cyan "debug" logger. */
|
||||||
export const debug = scoped('debug', {
|
export const debug: LogFunction = scoped("debug", {
|
||||||
color: 'cyanBright',
|
color: "cyanBright",
|
||||||
debug: true,
|
debug: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Built in green "success" logger, uses a unicode Check instead of the word Success. */
|
/** Built in green "success" logger, uses a unicode Check instead of the word Success. */
|
||||||
export const success = /* @__PURE__ */ scoped(successSymbol, {
|
export const success: LogFunction = /* @__PURE__ */ scoped(successSymbol, {
|
||||||
id: 'success',
|
id: "success",
|
||||||
color: 'greenBright',
|
color: "greenBright",
|
||||||
coloredText: true,
|
coloredText: true,
|
||||||
boldText: true,
|
boldText: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
import chalk, { type ChalkInstance } from 'chalk';
|
import chalk, { type ChalkInstance } from "chalk";
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from "node:util";
|
||||||
import { type LogFunction, type CustomLoggerColor, type CustomLoggerOptions, type StringLike, getColor } from './internal.ts';
|
import {
|
||||||
import { formatErrorObj, formatStackTrace } from './error.ts';
|
type CustomLoggerColor,
|
||||||
import stripAnsi from 'strip-ansi';
|
type CustomLoggerOptions,
|
||||||
import { writeLine } from './Widget.ts';
|
getColor,
|
||||||
|
type LogFunction,
|
||||||
|
type StringLike,
|
||||||
|
} from "./internal.ts";
|
||||||
|
import { formatErrorObj, formatStackTrace } from "./error.ts";
|
||||||
|
import stripAnsi from "strip-ansi";
|
||||||
|
import { writeLine } from "./Widget.ts";
|
||||||
|
|
||||||
export { writeLine } from './Widget.ts';
|
export { writeLine } from "./Widget.ts";
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import { formatErrorObj, formatStackTrace, CLIError, platformSimplifyErrorPath } from "./error.ts";
|
import {
|
||||||
|
CLIError,
|
||||||
|
formatErrorObj,
|
||||||
|
formatStackTrace,
|
||||||
|
platformSimplifyErrorPath,
|
||||||
|
} from "./error.ts";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
test("formatErrorObj formats basic errors", () => {
|
test("formatErrorObj formats basic errors", () => {
|
||||||
|
@ -16,9 +21,9 @@ test("formatErrorObj with PrintableError interface", () => {
|
||||||
const printableError = new Error("Custom message") as any;
|
const printableError = new Error("Custom message") as any;
|
||||||
printableError.name = "CustomError";
|
printableError.name = "CustomError";
|
||||||
printableError.description = "This is a detailed description of the error";
|
printableError.description = "This is a detailed description of the error";
|
||||||
|
|
||||||
const formatted = formatErrorObj(printableError);
|
const formatted = formatErrorObj(printableError);
|
||||||
|
|
||||||
// Should contain name, message and description
|
// Should contain name, message and description
|
||||||
expect(formatted).toContain("CustomError: Custom message");
|
expect(formatted).toContain("CustomError: Custom message");
|
||||||
expect(formatted).toContain("This is a detailed description of the error");
|
expect(formatted).toContain("This is a detailed description of the error");
|
||||||
|
@ -28,9 +33,9 @@ test("formatErrorObj with PrintableError interface", () => {
|
||||||
test("formatErrorObj with hideName option", () => {
|
test("formatErrorObj with hideName option", () => {
|
||||||
const printableError = new Error("Just the message") as any;
|
const printableError = new Error("Just the message") as any;
|
||||||
printableError.hideName = true;
|
printableError.hideName = true;
|
||||||
|
|
||||||
const formatted = formatErrorObj(printableError);
|
const formatted = formatErrorObj(printableError);
|
||||||
|
|
||||||
// Should not contain "Error:"
|
// Should not contain "Error:"
|
||||||
expect(formatted).not.toContain("Error:");
|
expect(formatted).not.toContain("Error:");
|
||||||
// Don't check the exact output since the stack trace might vary
|
// Don't check the exact output since the stack trace might vary
|
||||||
|
@ -39,9 +44,9 @@ test("formatErrorObj with hideName option", () => {
|
||||||
test("formatErrorObj with hideStack option", () => {
|
test("formatErrorObj with hideStack option", () => {
|
||||||
const printableError = new Error("No stack trace") as any;
|
const printableError = new Error("No stack trace") as any;
|
||||||
printableError.hideStack = true;
|
printableError.hideStack = true;
|
||||||
|
|
||||||
const formatted = formatErrorObj(printableError);
|
const formatted = formatErrorObj(printableError);
|
||||||
|
|
||||||
// Should contain message but not stack trace
|
// Should contain message but not stack trace
|
||||||
expect(formatted).toContain("Error: No stack trace");
|
expect(formatted).toContain("Error: No stack trace");
|
||||||
expect(formatted).not.toContain("at <anonymous>");
|
expect(formatted).not.toContain("at <anonymous>");
|
||||||
|
@ -50,7 +55,7 @@ test("formatErrorObj with hideStack option", () => {
|
||||||
test("formatStackTrace with V8 style stack", () => {
|
test("formatStackTrace with V8 style stack", () => {
|
||||||
const error = new Error("Stack trace test");
|
const error = new Error("Stack trace test");
|
||||||
const formatted = formatStackTrace(error);
|
const formatted = formatStackTrace(error);
|
||||||
|
|
||||||
// Should format the stack trace
|
// Should format the stack trace
|
||||||
expect(formatted).toContain("at ");
|
expect(formatted).toContain("at ");
|
||||||
// May not have color codes in test environment
|
// May not have color codes in test environment
|
||||||
|
@ -59,9 +64,9 @@ test("formatStackTrace with V8 style stack", () => {
|
||||||
test("formatStackTrace with empty stack", () => {
|
test("formatStackTrace with empty stack", () => {
|
||||||
const error = new Error("No stack");
|
const error = new Error("No stack");
|
||||||
error.stack = undefined;
|
error.stack = undefined;
|
||||||
|
|
||||||
const formatted = formatStackTrace(error);
|
const formatted = formatStackTrace(error);
|
||||||
|
|
||||||
// Should handle undefined stack
|
// Should handle undefined stack
|
||||||
expect(formatted).toEqual("");
|
expect(formatted).toEqual("");
|
||||||
});
|
});
|
||||||
|
@ -70,7 +75,7 @@ test("platformSimplifyErrorPath with cwd", () => {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const fullPath = `${cwd}/src/error.ts`;
|
const fullPath = `${cwd}/src/error.ts`;
|
||||||
const simplified = platformSimplifyErrorPath(fullPath);
|
const simplified = platformSimplifyErrorPath(fullPath);
|
||||||
|
|
||||||
// Should convert to a relative path
|
// Should convert to a relative path
|
||||||
expect(simplified).toEqual("./src/error.ts");
|
expect(simplified).toEqual("./src/error.ts");
|
||||||
});
|
});
|
||||||
|
@ -78,7 +83,7 @@ test("platformSimplifyErrorPath with cwd", () => {
|
||||||
test("platformSimplifyErrorPath with external path", () => {
|
test("platformSimplifyErrorPath with external path", () => {
|
||||||
const externalPath = "/usr/local/lib/node_modules/module.js";
|
const externalPath = "/usr/local/lib/node_modules/module.js";
|
||||||
const simplified = platformSimplifyErrorPath(externalPath);
|
const simplified = platformSimplifyErrorPath(externalPath);
|
||||||
|
|
||||||
// Should keep external paths unchanged
|
// Should keep external paths unchanged
|
||||||
expect(simplified).toEqual(externalPath);
|
expect(simplified).toEqual(externalPath);
|
||||||
});
|
});
|
||||||
|
@ -86,18 +91,22 @@ test("platformSimplifyErrorPath with external path", () => {
|
||||||
test("CLIError class", () => {
|
test("CLIError class", () => {
|
||||||
const cliError = new CLIError(
|
const cliError = new CLIError(
|
||||||
"Command failed",
|
"Command failed",
|
||||||
"Try running with --help for available options"
|
"Try running with --help for available options",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(cliError.message).toEqual("Command failed");
|
expect(cliError.message).toEqual("Command failed");
|
||||||
expect(cliError.description).toEqual("Try running with --help for available options");
|
expect(cliError.description).toEqual(
|
||||||
|
"Try running with --help for available options",
|
||||||
|
);
|
||||||
expect(cliError.hideStack).toEqual(true);
|
expect(cliError.hideStack).toEqual(true);
|
||||||
expect(cliError.hideName).toEqual(true);
|
expect(cliError.hideName).toEqual(true);
|
||||||
|
|
||||||
const formatted = formatErrorObj(cliError);
|
const formatted = formatErrorObj(cliError);
|
||||||
|
|
||||||
// Should format according to the PrintableError interface
|
// Should format according to the PrintableError interface
|
||||||
expect(formatted).toEqual("Command failed\nTry running with --help for available options\n");
|
expect(formatted).toEqual(
|
||||||
|
"Command failed\nTry running with --help for available options\n",
|
||||||
|
);
|
||||||
expect(formatted).not.toContain("CLIError");
|
expect(formatted).not.toContain("CLIError");
|
||||||
expect(formatted).not.toContain("at ");
|
expect(formatted).not.toContain("at ");
|
||||||
});
|
});
|
||||||
|
@ -109,9 +118,9 @@ test("pretty printing of objects in stack traces", () => {
|
||||||
JSON.parse("{invalid}");
|
JSON.parse("{invalid}");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const formatted = formatErrorObj(error as Error);
|
const formatted = formatErrorObj(error as Error);
|
||||||
|
|
||||||
// Should format the error message
|
// Should format the error message
|
||||||
expect(formatted).toContain("SyntaxError");
|
expect(formatted).toContain("SyntaxError");
|
||||||
expect(formatted).toContain("at ");
|
expect(formatted).toContain("at ");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
113
src/error.ts
113
src/error.ts
|
@ -1,11 +1,11 @@
|
||||||
import chalk from 'chalk';
|
import chalk from "chalk";
|
||||||
import path from 'node:path';
|
import path from "node:path";
|
||||||
import { isBuiltin } from 'node:module';
|
import { isBuiltin } from "node:module";
|
||||||
|
|
||||||
export function platformSimplifyErrorPath(filepath: string) {
|
export function platformSimplifyErrorPath(filepath: string): string {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
if (filepath.startsWith(cwd)) {
|
if (filepath.startsWith(cwd)) {
|
||||||
return '.' + filepath.slice(cwd.length);
|
return "." + filepath.slice(cwd.length);
|
||||||
}
|
}
|
||||||
return filepath;
|
return filepath;
|
||||||
}
|
}
|
||||||
|
@ -25,60 +25,65 @@ export interface PrintableError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Utility function we use internally for formatting the stack trace of an error. */
|
/** Utility function we use internally for formatting the stack trace of an error. */
|
||||||
export function formatStackTrace(err: Error) {
|
export function formatStackTrace(err: Error): string {
|
||||||
if (!err.stack) {
|
if (!err.stack) {
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
const v8firstLine = `${err.name}${err.message ? ': ' + err.message : ''}\n`;
|
const v8firstLine = `${err.name}${err.message ? ": " + err.message : ""}\n`;
|
||||||
const parsed = err.stack.startsWith(v8firstLine)
|
const parsed = err.stack.startsWith(v8firstLine)
|
||||||
? err.stack
|
? err.stack
|
||||||
.slice(v8firstLine.length)
|
.slice(v8firstLine.length)
|
||||||
.split('\n')
|
.split("\n")
|
||||||
.map(line => {
|
.map((line) => {
|
||||||
const match = /at (.*) \((.*):(\d+):(\d+)\)/.exec(line);
|
const match = /at (.*) \((.*):(\d+):(\d+)\)/.exec(line);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
const match2 = /at (.*):(\d+):(\d+)/.exec(line);
|
const match2 = /at (.*):(\d+):(\d+)/.exec(line);
|
||||||
if (match2) {
|
if (match2) {
|
||||||
return {
|
return {
|
||||||
method: '<top level>',
|
method: "<top level>",
|
||||||
file: match2[1],
|
file: match2[1],
|
||||||
line: match2[2],
|
line: match2[2],
|
||||||
column: match2[3],
|
column: match2[3],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { method: '<unknown>', file: null, line: null, column: null };
|
return {
|
||||||
|
method: "<unknown>",
|
||||||
|
file: null,
|
||||||
|
line: null,
|
||||||
|
column: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
method: match[1],
|
method: match[1],
|
||||||
file: match[2],
|
file: match[2],
|
||||||
line: parseInt(match[3] ?? '0', 10),
|
line: parseInt(match[3] ?? "0", 10),
|
||||||
column: parseInt(match[4] ?? '0', 10),
|
column: parseInt(match[4] ?? "0", 10),
|
||||||
native: line.includes('[native code]'),
|
native: line.includes("[native code]"),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: err.stack.split('\n').map(line => {
|
: err.stack.split("\n").map((line) => {
|
||||||
const at = line.indexOf('@');
|
const at = line.indexOf("@");
|
||||||
const method = line.slice(0, at);
|
const method = line.slice(0, at);
|
||||||
const file = line.slice(at + 1);
|
const file = line.slice(at + 1);
|
||||||
const fileSplit = /^(.*?):(\d+):(\d+)$/.exec(file);
|
const fileSplit = /^(.*?):(\d+):(\d+)$/.exec(file);
|
||||||
return {
|
return {
|
||||||
method: (['module code'].includes(method) ? '' : method) || '',
|
method: (["module code"].includes(method) ? "" : method) || "",
|
||||||
file: fileSplit ? platformSimplifyErrorPath(fileSplit[1] ?? '') : null,
|
file: fileSplit ? platformSimplifyErrorPath(fileSplit[1] ?? "") : null,
|
||||||
line: fileSplit ? parseInt(fileSplit[2] ?? '0', 10) : null,
|
line: fileSplit ? parseInt(fileSplit[2] ?? "0", 10) : null,
|
||||||
column: fileSplit ? parseInt(fileSplit[3] ?? '0', 10) : null,
|
column: fileSplit ? parseInt(fileSplit[3] ?? "0", 10) : null,
|
||||||
native: file === '[native code]',
|
native: file === "[native code]",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodeModuleJobIndex = parsed.findIndex(
|
const nodeModuleJobIndex = parsed.findIndex(
|
||||||
line => line.file === 'node:internal/modules/esm/module_job'
|
(line) => line.file === "node:internal/modules/esm/module_job",
|
||||||
);
|
);
|
||||||
if (nodeModuleJobIndex !== -1) {
|
if (nodeModuleJobIndex !== -1) {
|
||||||
parsed.splice(nodeModuleJobIndex, Infinity);
|
parsed.splice(nodeModuleJobIndex, Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed.reverse();
|
parsed.reverse();
|
||||||
const sliceAt = parsed.findIndex(line => !line.native);
|
const sliceAt = parsed.findIndex((line) => !line.native);
|
||||||
if (sliceAt !== -1) {
|
if (sliceAt !== -1) {
|
||||||
// remove the first native lines
|
// remove the first native lines
|
||||||
parsed.splice(0, sliceAt);
|
parsed.splice(0, sliceAt);
|
||||||
|
@ -88,12 +93,11 @@ export function formatStackTrace(err: Error) {
|
||||||
return parsed
|
return parsed
|
||||||
.map(({ method, file, line, column, native }) => {
|
.map(({ method, file, line, column, native }) => {
|
||||||
function getColoredDirname(filename: string) {
|
function getColoredDirname(filename: string) {
|
||||||
const dirname =
|
const dirname = process.platform === "win32"
|
||||||
process.platform === 'win32'
|
? path.dirname(filename).replace(/^file:\/\/\//g, "")
|
||||||
? path.dirname(filename).replace(/^file:\/\/\//g, '')
|
: path.dirname(filename).replace(/^file:\/\//g, "") + path.sep;
|
||||||
: path.dirname(filename).replace(/^file:\/\//g, '') + path.sep;
|
|
||||||
|
|
||||||
if (dirname === '/' || dirname === './') {
|
if (dirname === "/" || dirname === "./") {
|
||||||
return dirname;
|
return dirname;
|
||||||
}
|
}
|
||||||
return chalk.cyan(dirname);
|
return chalk.cyan(dirname);
|
||||||
|
@ -102,35 +106,36 @@ export function formatStackTrace(err: Error) {
|
||||||
const source = native
|
const source = native
|
||||||
? `[native code]`
|
? `[native code]`
|
||||||
: file
|
: file
|
||||||
? isBuiltin(file)
|
? isBuiltin(file) ? `(${chalk.magenta(file)})` : [
|
||||||
? `(${chalk.magenta(file)})`
|
"(",
|
||||||
: [
|
getColoredDirname(file),
|
||||||
'(',
|
// Leave the first slash on linux.
|
||||||
getColoredDirname(file),
|
chalk.green(path.basename(file)),
|
||||||
// Leave the first slash on linux.
|
":",
|
||||||
chalk.green(path.basename(file)),
|
line + ":" + column,
|
||||||
':',
|
")",
|
||||||
line + ':' + column,
|
].join("")
|
||||||
')',
|
: "<unknown>";
|
||||||
].join('')
|
|
||||||
: '<unknown>';
|
|
||||||
|
|
||||||
return chalk.blackBright(` at ${method === '' ? '' : `${method} `}${source}`);
|
return chalk.blackBright(
|
||||||
|
` at ${method === "" ? "" : `${method} `}${source}`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Formats the given error as a full log string. */
|
/** Formats the given error as a full log string. */
|
||||||
export function formatErrorObj(err: Error | PrintableError) {
|
export function formatErrorObj(err: Error | PrintableError): string {
|
||||||
const { name, message, description, hideStack, hideName, stack } = err as PrintableError;
|
const { name, message, description, hideStack, hideName, stack } =
|
||||||
|
err as PrintableError;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
hideName ? '' : (name ?? 'Error') + ': ',
|
hideName ? "" : (name ?? "Error") + ": ",
|
||||||
message ?? 'Unknown error',
|
message ?? "Unknown error",
|
||||||
description ? '\n' + description : '',
|
description ? "\n" + description : "",
|
||||||
hideStack || !stack ? '' : '\n' + chalk.reset(formatStackTrace(err)),
|
hideStack || !stack ? "" : "\n" + chalk.reset(formatStackTrace(err)),
|
||||||
description || (!hideStack && stack) ? '\n' : '',
|
description || (!hideStack && stack) ? "\n" : "",
|
||||||
].join('');
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,15 +163,15 @@ export class CLIError extends Error implements PrintableError {
|
||||||
|
|
||||||
constructor(message: string, description: string) {
|
constructor(message: string, description: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'CLIError';
|
this.name = "CLIError";
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hideStack() {
|
get hideStack(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hideName() {
|
get hideName(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,36 @@
|
||||||
import { beforeEach, expect, test, mock } from "bun:test";
|
import { beforeEach, expect, mock, test } from "bun:test";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
// We need to use a subprocess to test injection since it happens immediately on import
|
// We need to use a subprocess to test injection since it happens immediately on import
|
||||||
// and would affect our test environment
|
// and would affect our test environment
|
||||||
|
|
||||||
function runSubprocess(testType: string, env: Record<string, string> = {}): Promise<string> {
|
function runSubprocess(
|
||||||
|
testType: string,
|
||||||
|
env: Record<string, string> = {},
|
||||||
|
): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Merge the current process.env with any additional env variables
|
// Merge the current process.env with any additional env variables
|
||||||
const environment = { ...process.env, ...env };
|
const environment = { ...process.env, ...env };
|
||||||
|
|
||||||
const subprocess = spawn("bun", ["src/test/subprocess.ts", testType], {
|
const subprocess = spawn("bun", ["src/test/subprocess.ts", testType], {
|
||||||
env: environment
|
env: environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|
||||||
subprocess.stdout.on("data", (data) => {
|
subprocess.stdout.on("data", (data) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
subprocess.stderr.on("data", (data) => {
|
subprocess.stderr.on("data", (data) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
subprocess.on("error", (error) => {
|
subprocess.on("error", (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
subprocess.on("close", (code) => {
|
subprocess.on("close", (code) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
reject(new Error(`Subprocess exited with code ${code}: ${stderr}`));
|
reject(new Error(`Subprocess exited with code ${code}: ${stderr}`));
|
||||||
|
@ -40,18 +43,18 @@ function runSubprocess(testType: string, env: Record<string, string> = {}): Prom
|
||||||
|
|
||||||
test("basic injection is applied correctly", async () => {
|
test("basic injection is applied correctly", async () => {
|
||||||
// Enable debug logs with DEBUG=1 and force color with FORCE_COLOR
|
// Enable debug logs with DEBUG=1 and force color with FORCE_COLOR
|
||||||
const output = await runSubprocess("basic-injection", {
|
const output = await runSubprocess("basic-injection", {
|
||||||
DEBUG: "1",
|
DEBUG: "1",
|
||||||
FORCE_COLOR: "1"
|
FORCE_COLOR: "1",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that console methods are correctly injected
|
// Check that console methods are correctly injected
|
||||||
expect(output).toContain("INJECTED_LOG");
|
expect(output).toContain("INJECTED_LOG");
|
||||||
expect(output).toContain("INJECTED_INFO");
|
expect(output).toContain("INJECTED_INFO");
|
||||||
expect(output).toContain("INJECTED_WARN");
|
expect(output).toContain("INJECTED_WARN");
|
||||||
expect(output).toContain("INJECTED_ERROR");
|
expect(output).toContain("INJECTED_ERROR");
|
||||||
expect(output).toContain("INJECTED_DEBUG");
|
expect(output).toContain("INJECTED_DEBUG");
|
||||||
|
|
||||||
// Assert should only output when condition is false
|
// Assert should only output when condition is false
|
||||||
expect(output).toContain("INJECTED_ASSERT");
|
expect(output).toContain("INJECTED_ASSERT");
|
||||||
expect(output).not.toContain("This should not appear");
|
expect(output).not.toContain("This should not appear");
|
||||||
|
@ -59,18 +62,18 @@ test("basic injection is applied correctly", async () => {
|
||||||
|
|
||||||
test("timer functions work correctly", async () => {
|
test("timer functions work correctly", async () => {
|
||||||
const output = await runSubprocess("time-functions");
|
const output = await runSubprocess("time-functions");
|
||||||
|
|
||||||
// Check timer functions
|
// Check timer functions
|
||||||
expect(output).toContain("TIMER_LABEL");
|
expect(output).toContain("TIMER_LABEL");
|
||||||
expect(output).toContain("ms]"); // Time output should include milliseconds
|
expect(output).toContain("ms]"); // Time output should include milliseconds
|
||||||
|
|
||||||
// Warning for non-existent timer
|
// Warning for non-existent timer
|
||||||
expect(output).toContain("does not exist");
|
expect(output).toContain("does not exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("count functions work correctly", async () => {
|
test("count functions work correctly", async () => {
|
||||||
const output = await runSubprocess("count-functions");
|
const output = await runSubprocess("count-functions");
|
||||||
|
|
||||||
// Check counter functions
|
// Check counter functions
|
||||||
expect(output).toContain("COUNTER_LABEL: 1");
|
expect(output).toContain("COUNTER_LABEL: 1");
|
||||||
expect(output).toContain("COUNTER_LABEL: 2");
|
expect(output).toContain("COUNTER_LABEL: 2");
|
||||||
|
@ -79,9 +82,9 @@ test("count functions work correctly", async () => {
|
||||||
|
|
||||||
test("environment variables affect injection", async () => {
|
test("environment variables affect injection", async () => {
|
||||||
const output = await runSubprocess("disabled-injection", { NO_COLOR: "1" });
|
const output = await runSubprocess("disabled-injection", { NO_COLOR: "1" });
|
||||||
|
|
||||||
// When NO_COLOR is set, output should still have the message
|
// When NO_COLOR is set, output should still have the message
|
||||||
expect(output).toContain("DISABLED_COLOR_LOG");
|
expect(output).toContain("DISABLED_COLOR_LOG");
|
||||||
// Note: Chalk may still output color codes even with NO_COLOR=1 in some environments
|
// Note: Chalk may still output color codes even with NO_COLOR=1 in some environments
|
||||||
// So we're not checking for absence of color codes here
|
// So we're not checking for absence of color codes here
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import chalk from 'chalk';
|
import chalk from "chalk";
|
||||||
import { debug, error, info, trace, warn } from './console.ts';
|
import { debug, error, info, trace, warn } from "./console.ts";
|
||||||
import { Spinner } from './Spinner.ts';
|
import { Spinner } from "./Spinner.ts";
|
||||||
import { errorAllWidgets, writeLine } from './Widget.ts';
|
import { errorAllWidgets, writeLine } from "./Widget.ts";
|
||||||
|
|
||||||
// Basic Logging Functions
|
// Basic Logging Functions
|
||||||
console.log = info;
|
console.log = info;
|
||||||
|
@ -38,7 +38,9 @@ console.timeEnd = (label: string) => {
|
||||||
}
|
}
|
||||||
const { start, spinner } = timers.get(label)!;
|
const { start, spinner } = timers.get(label)!;
|
||||||
timers.delete(label);
|
timers.delete(label);
|
||||||
spinner.success(label + chalk.blackBright(` [${(performance.now() - start).toFixed(3)}ms]`));
|
spinner.success(
|
||||||
|
label + chalk.blackBright(` [${(performance.now() - start).toFixed(3)}ms]`),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
console.timeLog = (label: string) => {
|
console.timeLog = (label: string) => {
|
||||||
if (!timers.has(label)) {
|
if (!timers.has(label)) {
|
||||||
|
@ -46,7 +48,9 @@ console.timeLog = (label: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { start } = timers.get(label)!;
|
const { start } = timers.get(label)!;
|
||||||
console.log(label + chalk.blackBright(` [${(performance.now() - start).toFixed(3)}ms]`));
|
console.log(
|
||||||
|
label + chalk.blackBright(` [${(performance.now() - start).toFixed(3)}ms]`),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const counters = new Map<string, number>();
|
const counters = new Map<string, number>();
|
||||||
|
@ -61,20 +65,22 @@ console.countReset = (label: string) => {
|
||||||
|
|
||||||
console.trace = trace;
|
console.trace = trace;
|
||||||
|
|
||||||
process.on('uncaughtException', (exception: any) => {
|
process.on("uncaughtException", (exception: any) => {
|
||||||
errorAllWidgets('uncaught exception');
|
errorAllWidgets("uncaught exception");
|
||||||
|
|
||||||
error(exception);
|
error(exception);
|
||||||
|
|
||||||
writeLine('The above error was not caught by a catch block, execution cannot continue.');
|
writeLine(
|
||||||
|
"The above error was not caught by a catch block, execution cannot continue.",
|
||||||
|
);
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
process.on('unhandledRejection', (reason: any) => {
|
process.on("unhandledRejection", (reason: any) => {
|
||||||
errorAllWidgets('unhandled rejection');
|
errorAllWidgets("unhandled rejection");
|
||||||
error(reason);
|
error(reason);
|
||||||
writeLine(
|
writeLine(
|
||||||
'\nThe above error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()'
|
"\nThe above error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
246
src/internal.ts
246
src/internal.ts
|
@ -1,164 +1,170 @@
|
||||||
import chalk, { type ChalkInstance } from 'chalk';
|
import chalk, { type ChalkInstance } from "chalk";
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
export function convertHSVtoRGB(h: number, s: number, v: number): [number, number, number] {
|
export function convertHSVtoRGB(
|
||||||
let r, g, b;
|
h: number,
|
||||||
const i = Math.floor(h * 6);
|
s: number,
|
||||||
const f = h * 6 - i;
|
v: number,
|
||||||
const p = v * (1 - s);
|
): [number, number, number] {
|
||||||
const q = v * (1 - f * s);
|
let r, g, b;
|
||||||
const t = v * (1 - (1 - f) * s);
|
const i = Math.floor(h * 6);
|
||||||
switch (i % 6) {
|
const f = h * 6 - i;
|
||||||
case 0:
|
const p = v * (1 - s);
|
||||||
(r = v), (g = t), (b = p);
|
const q = v * (1 - f * s);
|
||||||
break;
|
const t = v * (1 - (1 - f) * s);
|
||||||
case 1:
|
switch (i % 6) {
|
||||||
(r = q), (g = v), (b = p);
|
case 0:
|
||||||
break;
|
(r = v), (g = t), (b = p);
|
||||||
case 2:
|
break;
|
||||||
(r = p), (g = v), (b = t);
|
case 1:
|
||||||
break;
|
(r = q), (g = v), (b = p);
|
||||||
case 3:
|
break;
|
||||||
(r = p), (g = q), (b = v);
|
case 2:
|
||||||
break;
|
(r = p), (g = v), (b = t);
|
||||||
case 4:
|
break;
|
||||||
(r = t), (g = p), (b = v);
|
case 3:
|
||||||
break;
|
(r = p), (g = q), (b = v);
|
||||||
case 5:
|
break;
|
||||||
(r = v), (g = p), (b = q);
|
case 4:
|
||||||
break;
|
(r = t), (g = p), (b = v);
|
||||||
default:
|
break;
|
||||||
return [0, 0, 0];
|
case 5:
|
||||||
}
|
(r = v), (g = p), (b = q);
|
||||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
break;
|
||||||
|
default:
|
||||||
|
return [0, 0, 0];
|
||||||
|
}
|
||||||
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Converts non string objects into a string the way Node.js' console.log does it. */
|
/** Converts non string objects into a string the way Node.js' console.log does it. */
|
||||||
export function stringify(...data: any[]) {
|
export function stringify(...data: any[]) {
|
||||||
return data.map(obj => (typeof obj === 'string' ? obj : inspect(obj, false, 4, true))).join(' ');
|
return data.map(
|
||||||
|
(obj) => (typeof obj === "string" ? obj : inspect(obj, false, 4, true)),
|
||||||
|
).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
let buffer = '';
|
let buffer = "";
|
||||||
let bufferNeedsUnfreeze = false;
|
let bufferNeedsUnfreeze = false;
|
||||||
let exiting = false;
|
let exiting = false;
|
||||||
|
|
||||||
export function writeToStderr(data: string, needsUnfreeze = false) {
|
export function writeToStderr(data: string, needsUnfreeze = false) {
|
||||||
buffer += data;
|
buffer += data;
|
||||||
if (exiting) flushStderr();
|
if (exiting) flushStderr();
|
||||||
if (needsUnfreeze) bufferNeedsUnfreeze = true;
|
if (needsUnfreeze) bufferNeedsUnfreeze = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stderr = process.stderr;
|
const stderr = process.stderr;
|
||||||
export function flushStderr() {
|
export function flushStderr() {
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
if (bufferNeedsUnfreeze) {
|
if (bufferNeedsUnfreeze) {
|
||||||
buffer += '\u001B[?2026l';
|
buffer += "\u001B[?2026l";
|
||||||
bufferNeedsUnfreeze = false;
|
bufferNeedsUnfreeze = false;
|
||||||
}
|
|
||||||
stderr.write(buffer);
|
|
||||||
buffer = '';
|
|
||||||
}
|
}
|
||||||
|
stderr.write(buffer);
|
||||||
|
buffer = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('exit', () => {
|
process.on("exit", () => {
|
||||||
exiting = true;
|
exiting = true;
|
||||||
buffer += '\x1b[0;39;49m';
|
buffer += "\x1b[0;39;49m";
|
||||||
flushStderr();
|
flushStderr();
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CustomLoggerColor =
|
export type CustomLoggerColor =
|
||||||
| 'black'
|
| "black"
|
||||||
| 'red'
|
| "red"
|
||||||
| 'green'
|
| "green"
|
||||||
| 'yellow'
|
| "yellow"
|
||||||
| 'blue'
|
| "blue"
|
||||||
| 'magenta'
|
| "magenta"
|
||||||
| 'cyan'
|
| "cyan"
|
||||||
| 'white'
|
| "white"
|
||||||
| 'gray'
|
| "gray"
|
||||||
| 'grey'
|
| "grey"
|
||||||
| 'blackBright'
|
| "blackBright"
|
||||||
| 'redBright'
|
| "redBright"
|
||||||
| 'greenBright'
|
| "greenBright"
|
||||||
| 'yellowBright'
|
| "yellowBright"
|
||||||
| 'blueBright'
|
| "blueBright"
|
||||||
| 'magentaBright'
|
| "magentaBright"
|
||||||
| 'cyanBright'
|
| "cyanBright"
|
||||||
| 'whiteBright'
|
| "whiteBright"
|
||||||
| `#${string}`
|
| `#${string}`
|
||||||
| number
|
| number
|
||||||
| [number, number, number];
|
| [number, number, number];
|
||||||
|
|
||||||
export interface CustomLoggerOptions {
|
export interface CustomLoggerOptions {
|
||||||
id?: string;
|
id?: string;
|
||||||
color?: CustomLoggerColor;
|
color?: CustomLoggerColor;
|
||||||
coloredText?: boolean;
|
coloredText?: boolean;
|
||||||
boldText?: boolean;
|
boldText?: boolean;
|
||||||
level?: number;
|
level?: number;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Matches `string`, `number`, and other objects with a `.toString()` method. */
|
/** Matches `string`, `number`, and other objects with a `.toString()` method. */
|
||||||
export interface StringLike {
|
export interface StringLike {
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormatStringArgs {
|
export interface FormatStringArgs {
|
||||||
'%s': StringLike | null | undefined;
|
"%s": StringLike | null | undefined;
|
||||||
'%d': number | null | undefined;
|
"%d": number | null | undefined;
|
||||||
'%i': number | null | undefined;
|
"%i": number | null | undefined;
|
||||||
'%f': number | null | undefined;
|
"%f": number | null | undefined;
|
||||||
'%x': number | null | undefined;
|
"%x": number | null | undefined;
|
||||||
'%X': number | null | undefined;
|
"%X": number | null | undefined;
|
||||||
'%o': any;
|
"%o": any;
|
||||||
'%O': any;
|
"%O": any;
|
||||||
'%c': string | null | undefined;
|
"%c": string | null | undefined;
|
||||||
'%j': any;
|
"%j": any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProcessFormatString<S> = S extends `${string}%${infer K}${infer B}`
|
export type ProcessFormatString<S> = S extends `${string}%${infer K}${infer B}`
|
||||||
? `%${K}` extends keyof FormatStringArgs
|
? `%${K}` extends keyof FormatStringArgs
|
||||||
? [FormatStringArgs[`%${K}`], ...ProcessFormatString<B>]
|
? [FormatStringArgs[`%${K}`], ...ProcessFormatString<B>]
|
||||||
: ProcessFormatString<B>
|
: ProcessFormatString<B>
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
export type LogData = string | number | boolean | object | null | undefined;
|
export type LogData = string | number | boolean | object | null | undefined;
|
||||||
|
|
||||||
export interface LogFunction {
|
export interface LogFunction {
|
||||||
/**
|
/**
|
||||||
* Writes data to the log. The first argument can be a printf-style format string, or usage
|
* Writes data to the log. The first argument can be a printf-style format string, or usage
|
||||||
* similar to `console.log`. Handles formatting objects including `Error` objects with pretty
|
* similar to `console.log`. Handles formatting objects including `Error` objects with pretty
|
||||||
* colorized stack traces.
|
* colorized stack traces.
|
||||||
*
|
*
|
||||||
* List of formatters:
|
* List of formatters:
|
||||||
*
|
*
|
||||||
* - %s - String.
|
* - %s - String.
|
||||||
* - %d, %f - Number.
|
* - %d, %f - Number.
|
||||||
* - %i - Integer.
|
* - %i - Integer.
|
||||||
* - %x - Hex.
|
* - %x - Hex.
|
||||||
* - %X - Hex (uppercase)
|
* - %X - Hex (uppercase)
|
||||||
* - %o - Object.
|
* - %o - Object.
|
||||||
* - %O - Object (pretty printed).
|
* - %O - Object (pretty printed).
|
||||||
* - %j - JSON.
|
* - %j - JSON.
|
||||||
*/
|
*/
|
||||||
<S extends LogData>(data?: S, ...a: ProcessFormatString<S>): void;
|
<S extends LogData>(data?: S, ...a: ProcessFormatString<S>): void;
|
||||||
/** Calling a logger function with no arguments prints a blank line. */
|
/** Calling a logger function with no arguments prints a blank line. */
|
||||||
(): void;
|
(): void;
|
||||||
|
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getColor(color: CustomLoggerColor): ChalkInstance {
|
export function getColor(color: CustomLoggerColor): ChalkInstance {
|
||||||
if (typeof color === 'string') {
|
if (typeof color === "string") {
|
||||||
if (color in chalk) {
|
if (color in chalk) {
|
||||||
return (chalk as any)[color];
|
return (chalk as any)[color];
|
||||||
} else if (color.startsWith('#') || color.match(/^#[0-9a-fA-F]{6}$/)) {
|
} else if (color.startsWith("#") || color.match(/^#[0-9a-fA-F]{6}$/)) {
|
||||||
return chalk.hex(color);
|
return chalk.hex(color);
|
||||||
}
|
|
||||||
throw new Error(`Invalid color: ${color}`);
|
|
||||||
} else if (Array.isArray(color)) {
|
|
||||||
return chalk.rgb(color[0], color[1], color[2]);
|
|
||||||
}
|
}
|
||||||
return chalk.ansi256(color);
|
throw new Error(`Invalid color: ${color}`);
|
||||||
|
} else if (Array.isArray(color)) {
|
||||||
|
return chalk.rgb(color[0], color[1], color[2]);
|
||||||
|
}
|
||||||
|
return chalk.ansi256(color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,53 +4,53 @@
|
||||||
// First argument tells what to test
|
// First argument tells what to test
|
||||||
const testType = process.argv[2];
|
const testType = process.argv[2];
|
||||||
|
|
||||||
if (testType === 'basic-injection') {
|
if (testType === "basic-injection") {
|
||||||
// Import and test basic injection
|
// Import and test basic injection
|
||||||
import('@paperclover/console/inject').then(() => {
|
import("@paperclover/console/inject").then(() => {
|
||||||
// Check that console methods are replaced
|
// Check that console methods are replaced
|
||||||
console.log('INJECTED_LOG');
|
console.log("INJECTED_LOG");
|
||||||
console.info('INJECTED_INFO');
|
console.info("INJECTED_INFO");
|
||||||
console.warn('INJECTED_WARN');
|
console.warn("INJECTED_WARN");
|
||||||
console.error('INJECTED_ERROR');
|
console.error("INJECTED_ERROR");
|
||||||
console.debug('INJECTED_DEBUG');
|
console.debug("INJECTED_DEBUG");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
console.assert(true, 'This should not appear');
|
console.assert(true, "This should not appear");
|
||||||
console.assert(false, 'INJECTED_ASSERT');
|
console.assert(false, "INJECTED_ASSERT");
|
||||||
|
|
||||||
// Exit when done
|
// Exit when done
|
||||||
setTimeout(() => process.exit(0), 100);
|
setTimeout(() => process.exit(0), 100);
|
||||||
});
|
});
|
||||||
} else if (testType === 'time-functions') {
|
} else if (testType === "time-functions") {
|
||||||
// Import and test timing functions
|
// Import and test timing functions
|
||||||
import('@paperclover/console/inject').then(() => {
|
import("@paperclover/console/inject").then(() => {
|
||||||
console.time('TIMER_LABEL');
|
console.time("TIMER_LABEL");
|
||||||
console.timeLog('TIMER_LABEL');
|
console.timeLog("TIMER_LABEL");
|
||||||
console.timeEnd('TIMER_LABEL');
|
console.timeEnd("TIMER_LABEL");
|
||||||
|
|
||||||
// Test invalid timer
|
// Test invalid timer
|
||||||
console.timeEnd('NONEXISTENT_TIMER');
|
console.timeEnd("NONEXISTENT_TIMER");
|
||||||
|
|
||||||
// Exit when done
|
// Exit when done
|
||||||
setTimeout(() => process.exit(0), 100);
|
setTimeout(() => process.exit(0), 100);
|
||||||
});
|
});
|
||||||
} else if (testType === 'count-functions') {
|
} else if (testType === "count-functions") {
|
||||||
// Import and test count functions
|
// Import and test count functions
|
||||||
import('@paperclover/console/inject').then(() => {
|
import("@paperclover/console/inject").then(() => {
|
||||||
console.count('COUNTER_LABEL');
|
console.count("COUNTER_LABEL");
|
||||||
console.count('COUNTER_LABEL');
|
console.count("COUNTER_LABEL");
|
||||||
console.countReset('COUNTER_LABEL');
|
console.countReset("COUNTER_LABEL");
|
||||||
console.count('COUNTER_LABEL');
|
console.count("COUNTER_LABEL");
|
||||||
|
|
||||||
// Exit when done
|
// Exit when done
|
||||||
setTimeout(() => process.exit(0), 100);
|
setTimeout(() => process.exit(0), 100);
|
||||||
});
|
});
|
||||||
} else if (testType === 'disabled-injection') {
|
} else if (testType === "disabled-injection") {
|
||||||
// NO_COLOR should be passed via spawn options
|
// NO_COLOR should be passed via spawn options
|
||||||
import('@paperclover/console/inject').then(() => {
|
import("@paperclover/console/inject").then(() => {
|
||||||
console.log('DISABLED_COLOR_LOG');
|
console.log("DISABLED_COLOR_LOG");
|
||||||
|
|
||||||
// Exit when done
|
// Exit when done
|
||||||
setTimeout(() => process.exit(0), 100);
|
setTimeout(() => process.exit(0), 100);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,103 +1,103 @@
|
||||||
//! Mock `@paperclover/console/Widget` to make testing easier.
|
//! Mock `@paperclover/console/Widget` to make testing easier.
|
||||||
import { mock, expect } from "bun:test";
|
import { expect, mock } from "bun:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import type { Widget } from "../Widget.ts";
|
import type { Widget } from "../Widget.ts";
|
||||||
|
|
||||||
process.env.CI = 'true';
|
process.env.CI = "true";
|
||||||
process.env.FORCE_COLOR = '10';
|
process.env.FORCE_COLOR = "10";
|
||||||
|
|
||||||
let buffer = '';
|
let buffer = "";
|
||||||
let widgets: MockWidget[] = [];
|
let widgets: MockWidget[] = [];
|
||||||
|
|
||||||
globalThis.setTimeout = (() => {
|
globalThis.setTimeout = (() => {
|
||||||
throw new Error('Do not call setTimeout in tests');
|
throw new Error("Do not call setTimeout in tests");
|
||||||
}) as any;
|
}) as any;
|
||||||
globalThis.clearTimeout = (() => {
|
globalThis.clearTimeout = (() => {
|
||||||
throw new Error('Do not call clearTimeout in tests');
|
throw new Error("Do not call clearTimeout in tests");
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
abstract class MockWidget {
|
abstract class MockWidget {
|
||||||
abstract format(now: number): string;
|
abstract format(now: number): string;
|
||||||
abstract fps: number;
|
abstract fps: number;
|
||||||
|
|
||||||
isActive = true;
|
isActive = true;
|
||||||
redrawCount = 0;
|
redrawCount = 0;
|
||||||
successCalls: string[] = [];
|
successCalls: string[] = [];
|
||||||
errorCalls: (string | Error)[] = [];
|
errorCalls: (string | Error)[] = [];
|
||||||
stopCalls: (string | undefined)[] = [];
|
stopCalls: (string | undefined)[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(finalMessage?: string): void {
|
stop(finalMessage?: string): void {
|
||||||
this.stopCalls.push(finalMessage);
|
this.stopCalls.push(finalMessage);
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected redraw(): void {
|
protected redraw(): void {
|
||||||
this.redrawCount++;
|
this.redrawCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
success(message: string): void {
|
success(message: string): void {
|
||||||
this.successCalls.push(message);
|
this.successCalls.push(message);
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: string | Error): void {
|
error(message: string | Error): void {
|
||||||
this.errorCalls.push(message);
|
this.errorCalls.push(message);
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get active(): boolean {
|
get active(): boolean {
|
||||||
return this.isActive;
|
return this.isActive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const warn = console.warn;
|
const warn = console.warn;
|
||||||
|
|
||||||
mock.module(require.resolve('../Widget.ts'), () => ({
|
mock.module(require.resolve("../Widget.ts"), () => ({
|
||||||
writeLine: (line: string) => buffer += line + '\n',
|
writeLine: (line: string) => buffer += line + "\n",
|
||||||
errorAllWidgets: () => {
|
errorAllWidgets: () => {
|
||||||
warn("errorAllWidgets is not implemented");
|
warn("errorAllWidgets is not implemented");
|
||||||
},
|
},
|
||||||
Widget: MockWidget,
|
Widget: MockWidget,
|
||||||
}));
|
}));
|
||||||
mock.module('node:fs', () => ({
|
mock.module("node:fs", () => ({
|
||||||
writeSync: (fd: number, data: string) => {
|
writeSync: (fd: number, data: string) => {
|
||||||
assert(fd === 2, 'writeSync must be called with stderr');
|
assert(fd === 2, "writeSync must be called with stderr");
|
||||||
buffer += data;
|
buffer += data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function reset() {
|
export function reset() {
|
||||||
buffer = '';
|
buffer = "";
|
||||||
widgets = [];
|
widgets = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBuffer() {
|
export function getBuffer() {
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWidgets() {
|
export function getWidgets() {
|
||||||
return widgets as any;
|
return widgets as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expectCalls(widget: Widget, checks: {
|
export function expectCalls(widget: Widget, checks: {
|
||||||
successCalls?: string[];
|
successCalls?: string[];
|
||||||
errorCalls?: (string | Error)[];
|
errorCalls?: (string | Error)[];
|
||||||
redraws?: number;
|
redraws?: number;
|
||||||
stopCalls?: (string | undefined)[];
|
stopCalls?: (string | undefined)[];
|
||||||
}) {
|
}) {
|
||||||
const mockWidget = widget as unknown as MockWidget;
|
const mockWidget = widget as unknown as MockWidget;
|
||||||
expect({
|
expect({
|
||||||
successCalls: mockWidget.successCalls,
|
successCalls: mockWidget.successCalls,
|
||||||
errorCalls: mockWidget.errorCalls,
|
errorCalls: mockWidget.errorCalls,
|
||||||
redraws: mockWidget.redrawCount,
|
redraws: mockWidget.redrawCount,
|
||||||
stopCalls: mockWidget.stopCalls,
|
stopCalls: mockWidget.stopCalls,
|
||||||
}).toEqual({
|
}).toEqual({
|
||||||
successCalls: checks.successCalls ?? [],
|
successCalls: checks.successCalls ?? [],
|
||||||
errorCalls: checks.errorCalls ?? [],
|
errorCalls: checks.errorCalls ?? [],
|
||||||
redraws: checks.redraws ?? 0,
|
redraws: checks.redraws ?? 0,
|
||||||
stopCalls: checks.stopCalls ?? [],
|
stopCalls: checks.stopCalls ?? [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue