@meadown/logger 1.8.9 → 1.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/core/createLog.js +2 -2
- package/dist/cjs/core/writeLog/helpers/formatLocation.d.ts +6 -0
- package/dist/cjs/core/writeLog/helpers/formatLocation.js +20 -0
- package/dist/cjs/core/writeLog/helpers/index.d.ts +3 -0
- package/dist/cjs/core/writeLog/helpers/index.js +17 -0
- package/dist/cjs/core/writeLog/helpers/renderMessage.d.ts +7 -0
- package/dist/cjs/core/writeLog/helpers/renderMessage.js +40 -0
- package/dist/cjs/core/writeLog/helpers/visibleLines.d.ts +6 -0
- package/dist/cjs/core/writeLog/helpers/visibleLines.js +22 -0
- package/dist/cjs/core/writeLog/index.d.ts +2 -0
- package/dist/cjs/core/writeLog/index.js +14 -0
- package/dist/cjs/core/{writeLog.d.ts → writeLog/writeLog.d.ts} +2 -6
- package/dist/cjs/core/writeLog/writeLog.js +43 -0
- package/dist/cjs/decorations/link.d.ts +2 -6
- package/dist/cjs/decorations/link.js +3 -8
- package/dist/cjs/index.d.ts +46 -17
- package/dist/cjs/index.js +20 -10
- package/dist/cjs/tap/createTap.js +6 -6
- package/dist/cjs/tap/tapAsync/helpers/buildBlock.d.ts +19 -0
- package/dist/cjs/tap/tapAsync/helpers/buildBlock.js +56 -0
- package/dist/cjs/tap/tapAsync/helpers/format.d.ts +4 -0
- package/dist/cjs/tap/tapAsync/helpers/format.js +30 -0
- package/dist/cjs/tap/tapAsync/helpers/index.d.ts +4 -0
- package/dist/cjs/tap/tapAsync/helpers/index.js +20 -0
- package/dist/cjs/tap/tapAsync/helpers/isThenable.d.ts +2 -0
- package/dist/cjs/tap/tapAsync/helpers/isThenable.js +15 -0
- package/dist/cjs/tap/tapAsync/helpers/response.d.ts +21 -0
- package/dist/cjs/tap/tapAsync/helpers/response.js +62 -0
- package/dist/cjs/tap/tapAsync/index.d.ts +2 -0
- package/dist/cjs/tap/tapAsync/index.js +13 -0
- package/dist/cjs/tap/{tapAsync.d.ts → tapAsync/tapAsync.d.ts} +1 -3
- package/dist/cjs/tap/tapAsync/tapAsync.js +92 -0
- package/dist/core/createLog.js +1 -1
- package/dist/core/writeLog/helpers/formatLocation.d.ts +6 -0
- package/dist/core/writeLog/helpers/formatLocation.js +17 -0
- package/dist/core/writeLog/helpers/index.d.ts +3 -0
- package/dist/core/writeLog/helpers/index.js +9 -0
- package/dist/core/writeLog/helpers/renderMessage.d.ts +7 -0
- package/dist/core/writeLog/helpers/renderMessage.js +37 -0
- package/dist/core/writeLog/helpers/visibleLines.d.ts +6 -0
- package/dist/core/writeLog/helpers/visibleLines.js +17 -0
- package/dist/core/writeLog/index.d.ts +2 -0
- package/dist/core/writeLog/index.js +8 -0
- package/dist/core/{writeLog.d.ts → writeLog/writeLog.d.ts} +2 -6
- package/dist/core/writeLog/writeLog.js +37 -0
- package/dist/decorations/link.d.ts +2 -6
- package/dist/decorations/link.js +3 -8
- package/dist/index.d.ts +46 -17
- package/dist/index.js +18 -8
- package/dist/tap/createTap.js +3 -3
- package/dist/tap/tapAsync/helpers/buildBlock.d.ts +19 -0
- package/dist/tap/tapAsync/helpers/buildBlock.js +53 -0
- package/dist/tap/tapAsync/helpers/format.d.ts +4 -0
- package/dist/tap/tapAsync/helpers/format.js +26 -0
- package/dist/tap/tapAsync/helpers/index.d.ts +4 -0
- package/dist/tap/tapAsync/helpers/index.js +10 -0
- package/dist/tap/tapAsync/helpers/isThenable.d.ts +2 -0
- package/dist/tap/tapAsync/helpers/isThenable.js +12 -0
- package/dist/tap/tapAsync/helpers/response.d.ts +21 -0
- package/dist/tap/tapAsync/helpers/response.js +57 -0
- package/dist/tap/tapAsync/index.d.ts +2 -0
- package/dist/tap/tapAsync/index.js +8 -0
- package/dist/tap/{tapAsync.d.ts → tapAsync/tapAsync.d.ts} +1 -3
- package/dist/tap/tapAsync/tapAsync.js +89 -0
- package/package.json +7 -4
- package/dist/cjs/core/writeLog.js +0 -92
- package/dist/cjs/tap/tapAsync.js +0 -192
- package/dist/core/writeLog.js +0 -84
- package/dist/tap/tapAsync.js +0 -188
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* index.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*/
|
|
7
|
+
export { readBody, isResponse, formatStatus, } from "./response.js";
|
|
8
|
+
export { buildBlock } from "./buildBlock.js";
|
|
9
|
+
export { isThenable } from "./isThenable.js";
|
|
10
|
+
export { formatDuration, formatBytes } from "./format.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* isThenable.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*/
|
|
7
|
+
/** Whether `value` is thenable (a promise we can await + time). */
|
|
8
|
+
export function isThenable(value) {
|
|
9
|
+
return (typeof value === "object" &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
typeof value.then === "function");
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ResponseLike = {
|
|
2
|
+
status: number;
|
|
3
|
+
statusText?: unknown;
|
|
4
|
+
headers?: {
|
|
5
|
+
get?: (name: string) => string | null;
|
|
6
|
+
};
|
|
7
|
+
clone: () => ResponseLike;
|
|
8
|
+
text: () => Promise<string>;
|
|
9
|
+
};
|
|
10
|
+
/** Whether the resolved value is a fetch `Response` we can read. */
|
|
11
|
+
export declare function isResponse(value: unknown): value is ResponseLike;
|
|
12
|
+
export declare function formatStatus(res: ResponseLike, useColor: boolean): string;
|
|
13
|
+
/**
|
|
14
|
+
* Reads a (cloned) response body and returns both the parsed value and the
|
|
15
|
+
* actual byte size. Size is calculated from the body text — not `Content-Length`,
|
|
16
|
+
* which is absent on compressed responses — so size is always shown.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readBody(res: ResponseLike): Promise<{
|
|
19
|
+
data: unknown;
|
|
20
|
+
size: string;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* response.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*/
|
|
7
|
+
import { formatBytes } from "./format.js";
|
|
8
|
+
import { colorize } from "../../../colors/color.js";
|
|
9
|
+
/** Whether the resolved value is a fetch `Response` we can read. */
|
|
10
|
+
export function isResponse(value) {
|
|
11
|
+
const v = value;
|
|
12
|
+
return (typeof v?.status === "number" &&
|
|
13
|
+
typeof v.clone === "function" &&
|
|
14
|
+
typeof v.text === "function");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Status badge color:
|
|
18
|
+
* 2xx → green · 3xx → cyan · 4xx → yellow · 5xx → red
|
|
19
|
+
*/
|
|
20
|
+
function statusColor(status) {
|
|
21
|
+
if (status >= 500)
|
|
22
|
+
return "red";
|
|
23
|
+
if (status >= 400)
|
|
24
|
+
return "yellow";
|
|
25
|
+
if (status >= 300)
|
|
26
|
+
return "cyan";
|
|
27
|
+
return "green";
|
|
28
|
+
}
|
|
29
|
+
export function formatStatus(res, useColor) {
|
|
30
|
+
const text = typeof res.statusText === "string" && res.statusText
|
|
31
|
+
? `${res.status} ${res.statusText}`
|
|
32
|
+
: `${res.status}`;
|
|
33
|
+
return useColor ? colorize(text, statusColor(res.status)) : text;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads a (cloned) response body and returns both the parsed value and the
|
|
37
|
+
* actual byte size. Size is calculated from the body text — not `Content-Length`,
|
|
38
|
+
* which is absent on compressed responses — so size is always shown.
|
|
39
|
+
*/
|
|
40
|
+
export async function readBody(res) {
|
|
41
|
+
try {
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
const bytes = new TextEncoder().encode(text).length;
|
|
44
|
+
const size = formatBytes(bytes);
|
|
45
|
+
if (text === "")
|
|
46
|
+
return { data: undefined, size };
|
|
47
|
+
try {
|
|
48
|
+
return { data: JSON.parse(text), size };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { data: text, size };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { data: undefined, size: "unknown" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { type Caller } from "
|
|
2
|
-
/** Whether `value` is thenable (a promise we can await + time). */
|
|
3
|
-
export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
|
|
1
|
+
import { type Caller } from "../../caller/getCaller.js";
|
|
4
2
|
/**
|
|
5
3
|
* The async tap. Fire-and-forget: returns `promise` immediately (unchanged),
|
|
6
4
|
* and logs a rich block once it resolves. For a `Response`, reads the body from
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* tapAsync.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*/
|
|
7
|
+
import { performance } from "node:perf_hooks";
|
|
8
|
+
import { formatDuration, formatBytes, isResponse, readBody, buildBlock, } from "./helpers/index.js";
|
|
9
|
+
import { isTTY } from "../../terminal/isTTY.js";
|
|
10
|
+
import { writeLog } from "../../core/writeLog/index.js";
|
|
11
|
+
/**
|
|
12
|
+
* The async tap. Fire-and-forget: returns `promise` immediately (unchanged),
|
|
13
|
+
* and logs a rich block once it resolves. For a `Response`, reads the body from
|
|
14
|
+
* a clone so the caller's original stays consumable. A rejection logs an error
|
|
15
|
+
* to stderr. `caller` MUST be resolved by `tap` (the user-facing function) so
|
|
16
|
+
* the logged location points at the user's file.
|
|
17
|
+
*/
|
|
18
|
+
export function tapAsync(promise, label, caller) {
|
|
19
|
+
const useColor = isTTY("stdout");
|
|
20
|
+
const start = performance.now();
|
|
21
|
+
void promise.then((resolved) => {
|
|
22
|
+
const ms = Math.round(performance.now() - start);
|
|
23
|
+
if (isResponse(resolved)) {
|
|
24
|
+
let clone = null;
|
|
25
|
+
try {
|
|
26
|
+
clone = resolved.clone();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
clone = null;
|
|
30
|
+
}
|
|
31
|
+
if (clone === null) {
|
|
32
|
+
writeLog({
|
|
33
|
+
channel: "log",
|
|
34
|
+
tag: "[TAP]",
|
|
35
|
+
args: buildBlock(label, ms, resolved, { data: undefined, size: "unknown" }, useColor),
|
|
36
|
+
caller,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const cl = resolved.headers?.get?.("content-length");
|
|
41
|
+
const tooLarge = cl != null && cl !== "" && Number(cl) > 512 * 1024;
|
|
42
|
+
if (tooLarge) {
|
|
43
|
+
writeLog({
|
|
44
|
+
channel: "log",
|
|
45
|
+
tag: "[TAP]",
|
|
46
|
+
args: buildBlock(label, ms, resolved, {
|
|
47
|
+
data: "(body too large to display)",
|
|
48
|
+
size: formatBytes(Number(cl)),
|
|
49
|
+
}, useColor),
|
|
50
|
+
caller,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
void readBody(clone).then((body) => {
|
|
55
|
+
writeLog({
|
|
56
|
+
channel: "log",
|
|
57
|
+
tag: "[TAP]",
|
|
58
|
+
args: buildBlock(label, ms, resolved, body, useColor),
|
|
59
|
+
caller,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Non-Response promise — plain value with elapsed time.
|
|
65
|
+
const elapsed = formatDuration(ms, useColor);
|
|
66
|
+
writeLog({
|
|
67
|
+
channel: "log",
|
|
68
|
+
tag: "[TAP]",
|
|
69
|
+
args: label === undefined
|
|
70
|
+
? [elapsed, resolved]
|
|
71
|
+
: [`${label} ${elapsed}`, resolved],
|
|
72
|
+
caller,
|
|
73
|
+
});
|
|
74
|
+
}, (err) => {
|
|
75
|
+
const ms = Math.round(performance.now() - start);
|
|
76
|
+
const elapsed = formatDuration(ms, useColor);
|
|
77
|
+
writeLog({
|
|
78
|
+
channel: "error",
|
|
79
|
+
tag: "[ERROR]",
|
|
80
|
+
args: [
|
|
81
|
+
label === undefined
|
|
82
|
+
? `rejected after ${elapsed}`
|
|
83
|
+
: `${label} rejected after ${elapsed}`,
|
|
84
|
+
err,
|
|
85
|
+
],
|
|
86
|
+
caller,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meadown/logger",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.11",
|
|
4
4
|
"description": "A development-focused logger for Node.js and TypeScript — zero dependencies, clickable source links, and API response logging built in.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logger",
|
|
@@ -53,9 +53,12 @@
|
|
|
53
53
|
"test": "tsc && node --test \"test/**/*.test.mjs\"",
|
|
54
54
|
"lint": "eslint src",
|
|
55
55
|
"lint:fix": "eslint src --fix",
|
|
56
|
-
"demo": "pnpm build && node examples/demo.
|
|
57
|
-
"demo:calls": "pnpm build && node examples/call-sites.
|
|
58
|
-
"demo:api": "pnpm build && node examples/api.
|
|
56
|
+
"demo": "pnpm build && node --experimental-strip-types examples/ts/demo.ts",
|
|
57
|
+
"demo:calls": "pnpm build && node --experimental-strip-types examples/ts/call-sites.ts",
|
|
58
|
+
"demo:api": "pnpm build && node --experimental-strip-types examples/ts/api.ts",
|
|
59
|
+
"demo-mjs": "pnpm build && node examples/mjs/demo.mjs",
|
|
60
|
+
"demo:calls-mjs": "pnpm build && node examples/mjs/call-sites.mjs",
|
|
61
|
+
"demo:api-mjs": "pnpm build && node examples/mjs/api.mjs",
|
|
59
62
|
"prepublishOnly": "pnpm run lint && pnpm test && pnpm run build",
|
|
60
63
|
"prepare": "husky",
|
|
61
64
|
"release": "semantic-release"
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* writeLog.ts
|
|
4
|
-
* Created by Dewan Mobashirul
|
|
5
|
-
* Copyright (c) 2026 dewan-meadown
|
|
6
|
-
* All rights reserved
|
|
7
|
-
*/
|
|
8
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.getVisibleLines = getVisibleLines;
|
|
13
|
-
exports.setVisibleLines = setVisibleLines;
|
|
14
|
-
exports.writeLog = writeLog;
|
|
15
|
-
const node_util_1 = require("node:util");
|
|
16
|
-
const constants_js_1 = require("../constants.js");
|
|
17
|
-
const getTimeStamp_js_1 = __importDefault(require("../time/getTimeStamp.js"));
|
|
18
|
-
const link_js_1 = require("../decorations/link.js");
|
|
19
|
-
const color_js_1 = require("../colors/color.js");
|
|
20
|
-
const isTTY_js_1 = require("../terminal/isTTY.js");
|
|
21
|
-
/** Max message lines to show before collapsing the rest; 0 (default) shows all. */
|
|
22
|
-
let visibleLines = constants_js_1.DEFAULT_MAX_LINES;
|
|
23
|
-
/** How many lines a long message shows before collapsing (0 = all). */
|
|
24
|
-
function getVisibleLines() {
|
|
25
|
-
return visibleLines;
|
|
26
|
-
}
|
|
27
|
-
/** Set how many lines a long message shows before collapsing (0 = all). */
|
|
28
|
-
function setVisibleLines(value) {
|
|
29
|
-
visibleLines = Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Collapses a long multi-line message to {@link visibleLines} lines, replacing
|
|
33
|
-
* the rest with a dimmed `… N more lines` summary. When `visibleLines` is 0
|
|
34
|
-
* (the default) nothing is collapsed — the full message is shown.
|
|
35
|
-
*/
|
|
36
|
-
function collapse(text, useColor) {
|
|
37
|
-
if (visibleLines < 1)
|
|
38
|
-
return text;
|
|
39
|
-
const lines = text.split("\n");
|
|
40
|
-
if (lines.length <= visibleLines)
|
|
41
|
-
return text;
|
|
42
|
-
const hidden = lines.length - visibleLines;
|
|
43
|
-
const summary = `${constants_js_1.MESSAGE_INDENT}... ${hidden} more line${hidden === 1 ? "" : "s"}`;
|
|
44
|
-
const visible = lines.slice(0, visibleLines);
|
|
45
|
-
visible.push(useColor ? (0, color_js_1.colorize)(summary, "gray") : summary);
|
|
46
|
-
return visible.join("\n");
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Renders the args into a single message string exactly as console would —
|
|
50
|
-
* objects/errors via util.inspect, `%s`/`%d` format specifiers, and colors when
|
|
51
|
-
* on a terminal — then hang-indents every continuation line so multi-line
|
|
52
|
-
* output stays left-aligned under the branch, and collapses very long output.
|
|
53
|
-
*/
|
|
54
|
-
function renderMessage(args, useColor) {
|
|
55
|
-
const text = (0, node_util_1.formatWithOptions)({ colors: useColor }, ...args);
|
|
56
|
-
return collapse(text.replace(/\n/g, `\n${(0, color_js_1.colorize)(constants_js_1.MESSAGE_INDENT, "gray")}`), useColor);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Renders a caller as a `(file:line)` location — a clickable OSC-8 link on a
|
|
60
|
-
* supporting terminal, plain text otherwise. Pure (no stack access).
|
|
61
|
-
*/
|
|
62
|
-
function formatLocation(caller, interactive) {
|
|
63
|
-
if (caller.file !== null && caller.line !== null && interactive)
|
|
64
|
-
return (0, link_js_1.hyperlink)(caller.label, (0, link_js_1.fileUrl)(caller.file, caller.line));
|
|
65
|
-
return caller.label;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Renders and writes one log entry. The `caller` is resolved by the *caller* of
|
|
69
|
-
* this function (the log closure or `tap`) and passed in, so this helper never
|
|
70
|
-
* touches the stack — keeping {@link getCaller}'s frame depth correct no matter
|
|
71
|
-
* which user-facing function delegates here.
|
|
72
|
-
*/
|
|
73
|
-
function writeLog(opts) {
|
|
74
|
-
const { channel, tag, args, caller } = opts;
|
|
75
|
-
const streamName = channel === "log" ? "stdout" : "stderr";
|
|
76
|
-
// One terminal check drives both color and clickable links — `isTTY` is the
|
|
77
|
-
// single source of truth (DRY). Off when output is piped/redirected.
|
|
78
|
-
const useColor = (0, isTTY_js_1.isTTY)(streamName);
|
|
79
|
-
const paint = (s, c) => useColor ? (0, color_js_1.colorize)(s, c) : s;
|
|
80
|
-
const location = formatLocation(caller, useColor);
|
|
81
|
-
const tagOut = paint(tag, constants_js_1.TAG_COLOR[channel]);
|
|
82
|
-
const timeStamp = paint((0, getTimeStamp_js_1.default)(), "teal");
|
|
83
|
-
const locOut = paint(`(${location})`, "dimTeal");
|
|
84
|
-
const connector = paint(constants_js_1.BRANCH, "gray");
|
|
85
|
-
const connectorBottom = paint(constants_js_1.BRANCH_END, "gray");
|
|
86
|
-
const separator = paint(constants_js_1.SEPARATOR, "gray");
|
|
87
|
-
// Layout: the tag, the message hanging off a `├──` branch, then the timestamp
|
|
88
|
-
// and location on a `└──` branch below. Leading `\n` spaces entries apart.
|
|
89
|
-
const message = renderMessage(args, useColor);
|
|
90
|
-
const meta = `\n${connectorBottom} ${timeStamp} ${separator} ${locOut}`;
|
|
91
|
-
console[channel](`\n${tagOut}`, `\n${connector}`, message, meta);
|
|
92
|
-
}
|
package/dist/cjs/tap/tapAsync.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* tapAsync.ts
|
|
4
|
-
* Created by Dewan Mobashirul
|
|
5
|
-
* Copyright (c) 2026 dewan-meadown
|
|
6
|
-
* All rights reserved
|
|
7
|
-
*
|
|
8
|
-
* The async side of `tap`: times a promise and logs a rich response block when
|
|
9
|
-
* it resolves to a `Response` — status color, slow-request highlighting, size,
|
|
10
|
-
* and the actual body — all fire-and-forget so the caller never waits.
|
|
11
|
-
*/
|
|
12
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.isThenable = isThenable;
|
|
14
|
-
exports.tapAsync = tapAsync;
|
|
15
|
-
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
16
|
-
const node_util_1 = require("node:util");
|
|
17
|
-
const writeLog_js_1 = require("../core/writeLog.js");
|
|
18
|
-
const color_js_1 = require("../colors/color.js");
|
|
19
|
-
const isTTY_js_1 = require("../terminal/isTTY.js");
|
|
20
|
-
/** Whether `value` is thenable (a promise we can await + time). */
|
|
21
|
-
function isThenable(value) {
|
|
22
|
-
return (typeof value === "object" &&
|
|
23
|
-
value !== null &&
|
|
24
|
-
typeof value.then === "function");
|
|
25
|
-
}
|
|
26
|
-
/** Whether the resolved value is a fetch `Response` we can read. */
|
|
27
|
-
function isResponse(value) {
|
|
28
|
-
const v = value;
|
|
29
|
-
return (typeof v?.status === "number" &&
|
|
30
|
-
typeof v.clone === "function" &&
|
|
31
|
-
typeof v.text === "function");
|
|
32
|
-
}
|
|
33
|
-
/** `65ms` (green) · `1.2s` (yellow) · `5.8s` (red). */
|
|
34
|
-
function formatDuration(ms, useColor) {
|
|
35
|
-
const text = ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
|
|
36
|
-
if (!useColor)
|
|
37
|
-
return text;
|
|
38
|
-
if (ms >= 2000)
|
|
39
|
-
return (0, color_js_1.colorize)(text, "red");
|
|
40
|
-
if (ms >= 500)
|
|
41
|
-
return (0, color_js_1.colorize)(text, "yellow");
|
|
42
|
-
return (0, color_js_1.colorize)(text, "green");
|
|
43
|
-
}
|
|
44
|
-
/** `848 B` / `1.84 KB` / `2.10 MB`. */
|
|
45
|
-
function formatBytes(bytes) {
|
|
46
|
-
if (bytes < 1024)
|
|
47
|
-
return `${bytes} B`;
|
|
48
|
-
if (bytes < 1024 * 1024)
|
|
49
|
-
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
50
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Status badge color:
|
|
54
|
-
* 2xx → green · 3xx → cyan · 4xx → yellow · 5xx → red
|
|
55
|
-
*/
|
|
56
|
-
function statusColor(status) {
|
|
57
|
-
if (status >= 500)
|
|
58
|
-
return "red";
|
|
59
|
-
if (status >= 400)
|
|
60
|
-
return "yellow";
|
|
61
|
-
if (status >= 300)
|
|
62
|
-
return "cyan";
|
|
63
|
-
return "green";
|
|
64
|
-
}
|
|
65
|
-
function formatStatus(res, useColor) {
|
|
66
|
-
const text = typeof res.statusText === "string" && res.statusText
|
|
67
|
-
? `${res.status} ${res.statusText}`
|
|
68
|
-
: `${res.status}`;
|
|
69
|
-
return useColor ? (0, color_js_1.colorize)(text, statusColor(res.status)) : text;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Reads a (cloned) response body and returns both the parsed value and the
|
|
73
|
-
* actual byte size. Size is calculated from the body text — not `Content-Length`,
|
|
74
|
-
* which is absent on compressed responses — so size is always shown.
|
|
75
|
-
*/
|
|
76
|
-
async function readBody(res) {
|
|
77
|
-
try {
|
|
78
|
-
const text = await res.text();
|
|
79
|
-
const bytes = new TextEncoder().encode(text).length;
|
|
80
|
-
const size = formatBytes(bytes);
|
|
81
|
-
if (text === "")
|
|
82
|
-
return { data: undefined, size };
|
|
83
|
-
try {
|
|
84
|
-
return { data: JSON.parse(text), size };
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return { data: text, size };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return { data: undefined, size: "unknown" };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Renders the nested tree block the user asked for:
|
|
96
|
-
*
|
|
97
|
-
* GET /users/1
|
|
98
|
-
* │
|
|
99
|
-
* │ response:
|
|
100
|
-
* │ ├── time: 65ms
|
|
101
|
-
* │ ├── status: 200 OK
|
|
102
|
-
* │ └── size: 848 B
|
|
103
|
-
* │
|
|
104
|
-
* │ body:
|
|
105
|
-
* │ ├── id: 1
|
|
106
|
-
* │ └── name: Leanne Graham
|
|
107
|
-
*/
|
|
108
|
-
function buildBlock(label, ms, res, body, useColor) {
|
|
109
|
-
const paint = (s, c) => useColor ? (0, color_js_1.colorize)(s, c) : s;
|
|
110
|
-
const pipe = paint("│", "gray");
|
|
111
|
-
const branch = paint("├──", "gray");
|
|
112
|
-
const last = paint("└──", "gray");
|
|
113
|
-
const indent = `${pipe} `;
|
|
114
|
-
const timeLine = `${indent}${branch} time: ${formatDuration(ms, useColor)}`;
|
|
115
|
-
const statusLine = `${indent}${branch} status: ${formatStatus(res, useColor)}`;
|
|
116
|
-
const sizeLine = `${indent}${last} size: ${body.size}`;
|
|
117
|
-
const responseLines = [`${pipe}`, `${indent}response:`, timeLine, statusLine, sizeLine];
|
|
118
|
-
const head = label === undefined ? "" : `${label}\n`;
|
|
119
|
-
if (body.data === undefined) {
|
|
120
|
-
return [`${head}${responseLines.join("\n")}`];
|
|
121
|
-
}
|
|
122
|
-
// Render the body using util.formatWithOptions so objects/arrays look like
|
|
123
|
-
// console.log output — colors, proper nesting, no JSON.stringify quirkiness.
|
|
124
|
-
const bodyText = (0, node_util_1.formatWithOptions)({ colors: useColor }, body.data);
|
|
125
|
-
const bodyLines = bodyText.split("\n");
|
|
126
|
-
const lastIdx = bodyLines.length - 1;
|
|
127
|
-
const bodyBlock = bodyLines
|
|
128
|
-
.map((line, i) => `${indent}${i === lastIdx ? last : branch} ${line}`)
|
|
129
|
-
.join("\n");
|
|
130
|
-
const full = [...responseLines, `${pipe}`, `${indent}body:`, bodyBlock].join("\n");
|
|
131
|
-
return [`${head}${full}`];
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* The async tap. Fire-and-forget: returns `promise` immediately (unchanged),
|
|
135
|
-
* and logs a rich block once it resolves. For a `Response`, reads the body from
|
|
136
|
-
* a clone so the caller's original stays consumable. A rejection logs an error
|
|
137
|
-
* to stderr. `caller` MUST be resolved by `tap` (the user-facing function) so
|
|
138
|
-
* the logged location points at the user's file.
|
|
139
|
-
*/
|
|
140
|
-
function tapAsync(promise, label, caller) {
|
|
141
|
-
const useColor = (0, isTTY_js_1.isTTY)("stdout");
|
|
142
|
-
const start = node_perf_hooks_1.performance.now();
|
|
143
|
-
void promise.then((resolved) => {
|
|
144
|
-
const ms = Math.round(node_perf_hooks_1.performance.now() - start);
|
|
145
|
-
if (isResponse(resolved)) {
|
|
146
|
-
let clone = null;
|
|
147
|
-
try {
|
|
148
|
-
clone = resolved.clone();
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
clone = null;
|
|
152
|
-
}
|
|
153
|
-
if (clone === null) {
|
|
154
|
-
(0, writeLog_js_1.writeLog)({
|
|
155
|
-
channel: "log", tag: "[TAP]",
|
|
156
|
-
args: buildBlock(label, ms, resolved, { data: undefined, size: "unknown" }, useColor),
|
|
157
|
-
caller,
|
|
158
|
-
});
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const cl = resolved.headers?.get?.("content-length");
|
|
162
|
-
const tooLarge = cl != null && cl !== "" && Number(cl) > 512 * 1024;
|
|
163
|
-
if (tooLarge) {
|
|
164
|
-
(0, writeLog_js_1.writeLog)({
|
|
165
|
-
channel: "log", tag: "[TAP]",
|
|
166
|
-
args: buildBlock(label, ms, resolved, { data: "(body too large to display)", size: formatBytes(Number(cl)) }, useColor),
|
|
167
|
-
caller,
|
|
168
|
-
});
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
void readBody(clone).then((body) => {
|
|
172
|
-
(0, writeLog_js_1.writeLog)({ channel: "log", tag: "[TAP]", args: buildBlock(label, ms, resolved, body, useColor), caller });
|
|
173
|
-
});
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
// Non-Response promise — plain value with elapsed time.
|
|
177
|
-
const elapsed = formatDuration(ms, useColor);
|
|
178
|
-
(0, writeLog_js_1.writeLog)({
|
|
179
|
-
channel: "log", tag: "[TAP]",
|
|
180
|
-
args: label === undefined ? [elapsed, resolved] : [`${label} ${elapsed}`, resolved],
|
|
181
|
-
caller,
|
|
182
|
-
});
|
|
183
|
-
}, (err) => {
|
|
184
|
-
const ms = Math.round(node_perf_hooks_1.performance.now() - start);
|
|
185
|
-
const elapsed = formatDuration(ms, useColor);
|
|
186
|
-
(0, writeLog_js_1.writeLog)({
|
|
187
|
-
channel: "error", tag: "[TAP]",
|
|
188
|
-
args: [label === undefined ? `rejected after ${elapsed}` : `${label} rejected after ${elapsed}`, err],
|
|
189
|
-
caller,
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
}
|
package/dist/core/writeLog.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* writeLog.ts
|
|
3
|
-
* Created by Dewan Mobashirul
|
|
4
|
-
* Copyright (c) 2026 dewan-meadown
|
|
5
|
-
* All rights reserved
|
|
6
|
-
*/
|
|
7
|
-
import { formatWithOptions } from "node:util";
|
|
8
|
-
import { TAG_COLOR, BRANCH, BRANCH_END, SEPARATOR, MESSAGE_INDENT, DEFAULT_MAX_LINES, } from "../constants.js";
|
|
9
|
-
import getTimeStamp from "../time/getTimeStamp.js";
|
|
10
|
-
import { fileUrl, hyperlink } from "../decorations/link.js";
|
|
11
|
-
import { colorize } from "../colors/color.js";
|
|
12
|
-
import { isTTY } from "../terminal/isTTY.js";
|
|
13
|
-
/** Max message lines to show before collapsing the rest; 0 (default) shows all. */
|
|
14
|
-
let visibleLines = DEFAULT_MAX_LINES;
|
|
15
|
-
/** How many lines a long message shows before collapsing (0 = all). */
|
|
16
|
-
export function getVisibleLines() {
|
|
17
|
-
return visibleLines;
|
|
18
|
-
}
|
|
19
|
-
/** Set how many lines a long message shows before collapsing (0 = all). */
|
|
20
|
-
export function setVisibleLines(value) {
|
|
21
|
-
visibleLines = Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Collapses a long multi-line message to {@link visibleLines} lines, replacing
|
|
25
|
-
* the rest with a dimmed `… N more lines` summary. When `visibleLines` is 0
|
|
26
|
-
* (the default) nothing is collapsed — the full message is shown.
|
|
27
|
-
*/
|
|
28
|
-
function collapse(text, useColor) {
|
|
29
|
-
if (visibleLines < 1)
|
|
30
|
-
return text;
|
|
31
|
-
const lines = text.split("\n");
|
|
32
|
-
if (lines.length <= visibleLines)
|
|
33
|
-
return text;
|
|
34
|
-
const hidden = lines.length - visibleLines;
|
|
35
|
-
const summary = `${MESSAGE_INDENT}... ${hidden} more line${hidden === 1 ? "" : "s"}`;
|
|
36
|
-
const visible = lines.slice(0, visibleLines);
|
|
37
|
-
visible.push(useColor ? colorize(summary, "gray") : summary);
|
|
38
|
-
return visible.join("\n");
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Renders the args into a single message string exactly as console would —
|
|
42
|
-
* objects/errors via util.inspect, `%s`/`%d` format specifiers, and colors when
|
|
43
|
-
* on a terminal — then hang-indents every continuation line so multi-line
|
|
44
|
-
* output stays left-aligned under the branch, and collapses very long output.
|
|
45
|
-
*/
|
|
46
|
-
function renderMessage(args, useColor) {
|
|
47
|
-
const text = formatWithOptions({ colors: useColor }, ...args);
|
|
48
|
-
return collapse(text.replace(/\n/g, `\n${colorize(MESSAGE_INDENT, "gray")}`), useColor);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Renders a caller as a `(file:line)` location — a clickable OSC-8 link on a
|
|
52
|
-
* supporting terminal, plain text otherwise. Pure (no stack access).
|
|
53
|
-
*/
|
|
54
|
-
function formatLocation(caller, interactive) {
|
|
55
|
-
if (caller.file !== null && caller.line !== null && interactive)
|
|
56
|
-
return hyperlink(caller.label, fileUrl(caller.file, caller.line));
|
|
57
|
-
return caller.label;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Renders and writes one log entry. The `caller` is resolved by the *caller* of
|
|
61
|
-
* this function (the log closure or `tap`) and passed in, so this helper never
|
|
62
|
-
* touches the stack — keeping {@link getCaller}'s frame depth correct no matter
|
|
63
|
-
* which user-facing function delegates here.
|
|
64
|
-
*/
|
|
65
|
-
export function writeLog(opts) {
|
|
66
|
-
const { channel, tag, args, caller } = opts;
|
|
67
|
-
const streamName = channel === "log" ? "stdout" : "stderr";
|
|
68
|
-
// One terminal check drives both color and clickable links — `isTTY` is the
|
|
69
|
-
// single source of truth (DRY). Off when output is piped/redirected.
|
|
70
|
-
const useColor = isTTY(streamName);
|
|
71
|
-
const paint = (s, c) => useColor ? colorize(s, c) : s;
|
|
72
|
-
const location = formatLocation(caller, useColor);
|
|
73
|
-
const tagOut = paint(tag, TAG_COLOR[channel]);
|
|
74
|
-
const timeStamp = paint(getTimeStamp(), "teal");
|
|
75
|
-
const locOut = paint(`(${location})`, "dimTeal");
|
|
76
|
-
const connector = paint(BRANCH, "gray");
|
|
77
|
-
const connectorBottom = paint(BRANCH_END, "gray");
|
|
78
|
-
const separator = paint(SEPARATOR, "gray");
|
|
79
|
-
// Layout: the tag, the message hanging off a `├──` branch, then the timestamp
|
|
80
|
-
// and location on a `└──` branch below. Leading `\n` spaces entries apart.
|
|
81
|
-
const message = renderMessage(args, useColor);
|
|
82
|
-
const meta = `\n${connectorBottom} ${timeStamp} ${separator} ${locOut}`;
|
|
83
|
-
console[channel](`\n${tagOut}`, `\n${connector}`, message, meta);
|
|
84
|
-
}
|