@meadown/logger 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -63
- package/SECURITY.md +30 -4
- package/dist/{utils → caller}/getCaller.js +0 -1
- package/dist/{utils → cjs/caller}/getCaller.d.ts +0 -1
- package/dist/cjs/colors/color.d.ts +20 -0
- package/dist/cjs/colors/color.js +29 -0
- package/dist/cjs/constants.d.ts +14 -0
- package/dist/cjs/constants.js +27 -0
- package/dist/cjs/core/createLog.d.ts +8 -0
- package/dist/cjs/core/createLog.js +29 -0
- package/dist/cjs/core/writeLog.d.ts +18 -0
- package/dist/cjs/core/writeLog.js +95 -0
- package/dist/cjs/{utils → decorations}/link.d.ts +0 -7
- package/dist/cjs/{utils → decorations}/link.js +0 -13
- package/dist/cjs/index.d.ts +9 -1
- package/dist/cjs/index.js +12 -6
- package/dist/cjs/tap/createTap.d.ts +18 -0
- package/dist/cjs/tap/createTap.js +51 -0
- package/dist/cjs/tap/tapAsync.d.ts +11 -0
- package/dist/cjs/tap/tapAsync.js +207 -0
- package/dist/cjs/terminal/isTTY.d.ts +7 -0
- package/dist/cjs/terminal/isTTY.js +21 -0
- package/dist/colors/color.d.ts +20 -0
- package/dist/colors/color.js +26 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +24 -0
- package/dist/core/createLog.d.ts +8 -0
- package/dist/core/createLog.js +23 -0
- package/dist/core/writeLog.d.ts +18 -0
- package/dist/core/writeLog.js +87 -0
- package/dist/{utils → decorations}/link.d.ts +0 -8
- package/dist/{utils → decorations}/link.js +0 -13
- package/dist/index.d.ts +9 -2
- package/dist/index.js +4 -2
- package/dist/tap/createTap.d.ts +18 -0
- package/dist/tap/createTap.js +45 -0
- package/dist/tap/tapAsync.d.ts +11 -0
- package/dist/tap/tapAsync.js +203 -0
- package/dist/terminal/isTTY.d.ts +7 -0
- package/dist/terminal/isTTY.js +18 -0
- package/dist/{utils → time}/getTimeStamp.d.ts +0 -1
- package/dist/{utils → time}/getTimeStamp.js +0 -1
- package/package.json +4 -2
- package/dist/cjs/utils/color.d.ts +0 -25
- package/dist/cjs/utils/color.js +0 -40
- package/dist/cjs/utils/createLog.d.ts +0 -14
- package/dist/cjs/utils/createLog.js +0 -116
- package/dist/cjs/utils/index.d.ts +0 -5
- package/dist/cjs/utils/index.js +0 -27
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/utils/color.d.ts +0 -26
- package/dist/utils/color.d.ts.map +0 -1
- package/dist/utils/color.js +0 -37
- package/dist/utils/color.js.map +0 -1
- package/dist/utils/createLog.d.ts +0 -15
- package/dist/utils/createLog.d.ts.map +0 -1
- package/dist/utils/createLog.js +0 -109
- package/dist/utils/createLog.js.map +0 -1
- package/dist/utils/getCaller.d.ts.map +0 -1
- package/dist/utils/getCaller.js.map +0 -1
- package/dist/utils/getTimeStamp.d.ts.map +0 -1
- package/dist/utils/getTimeStamp.js.map +0 -1
- package/dist/utils/index.d.ts +0 -6
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -12
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/link.d.ts.map +0 -1
- package/dist/utils/link.js.map +0 -1
- /package/dist/{cjs/utils → caller}/getCaller.d.ts +0 -0
- /package/dist/cjs/{utils → caller}/getCaller.js +0 -0
- /package/dist/cjs/{utils → time}/getTimeStamp.d.ts +0 -0
- /package/dist/cjs/{utils → time}/getTimeStamp.js +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* createTap.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.default = createTap;
|
|
13
|
+
const getCaller_js_1 = __importDefault(require("../caller/getCaller.js"));
|
|
14
|
+
const writeLog_js_1 = require("../core/writeLog.js");
|
|
15
|
+
const config_js_1 = require("../config.js");
|
|
16
|
+
const tapAsync_js_1 = require("./tapAsync.js");
|
|
17
|
+
/**
|
|
18
|
+
* Builds `tap` — logs a value and returns it **unchanged**, so it drops into any
|
|
19
|
+
* expression (`const u = logger.tap(getUser(), "user")`). The consumer always
|
|
20
|
+
* gets back exactly what they passed; the only effect is a clean log.
|
|
21
|
+
*
|
|
22
|
+
* - A plain value is logged synchronously and returned.
|
|
23
|
+
* - A **promise** is returned as-is (same object, never awaited or wrapped), and
|
|
24
|
+
* its elapsed time — plus the HTTP status if it resolves to a `Response` — is
|
|
25
|
+
* logged in the background (fire-and-forget, non-blocking).
|
|
26
|
+
*
|
|
27
|
+
* The returned closure is what the caller invokes directly, so {@link getCaller}
|
|
28
|
+
* resolves the caller's own frame. Silent in production; the value still flows.
|
|
29
|
+
*/
|
|
30
|
+
function createTap() {
|
|
31
|
+
const tap = (value, label) => {
|
|
32
|
+
if (!(0, config_js_1.isLogAllowed)())
|
|
33
|
+
return value;
|
|
34
|
+
const caller = (0, getCaller_js_1.default)();
|
|
35
|
+
if ((0, tapAsync_js_1.isThenable)(value)) {
|
|
36
|
+
// value is a promise → hand off to the async tap (caller passed in so the
|
|
37
|
+
// location stays on the user's file, not on this helper).
|
|
38
|
+
(0, tapAsync_js_1.tapAsync)(value, label, caller);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
(0, writeLog_js_1.writeLog)({
|
|
42
|
+
channel: "log",
|
|
43
|
+
tag: "[TAP]",
|
|
44
|
+
args: label === undefined ? [value] : [label, value],
|
|
45
|
+
caller,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
return tap;
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Caller } from "../caller/getCaller.js";
|
|
2
|
+
/** Whether `value` is thenable (a promise we can await + time). */
|
|
3
|
+
export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* The async tap. Fire-and-forget: returns `promise` immediately (unchanged),
|
|
6
|
+
* and logs a rich block once it resolves. For a `Response`, reads the body from
|
|
7
|
+
* a clone so the caller's original stays consumable. A rejection logs an error
|
|
8
|
+
* to stderr. `caller` MUST be resolved by `tap` (the user-facing function) so
|
|
9
|
+
* the logged location points at the user's file.
|
|
10
|
+
*/
|
|
11
|
+
export declare function tapAsync(promise: PromiseLike<unknown>, label: string | undefined, caller: Caller): void;
|
|
@@ -0,0 +1,207 @@
|
|
|
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 >= 3000)
|
|
39
|
+
return (0, color_js_1.colorize)(text, "red");
|
|
40
|
+
if (ms >= 1000)
|
|
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 pipe = useColor ? (0, color_js_1.colorize)("│", "gray") : "│";
|
|
110
|
+
const branch = useColor ? (0, color_js_1.colorize)("├──", "gray") : "├──";
|
|
111
|
+
const last = useColor ? (0, color_js_1.colorize)("└──", "gray") : "└──";
|
|
112
|
+
const indent = `${pipe} `;
|
|
113
|
+
const statusLine = `${indent}${branch} status: ${formatStatus(res, useColor)}`;
|
|
114
|
+
const timeLine = `${indent}${branch} time: ${formatDuration(ms, useColor)}`;
|
|
115
|
+
const sizeLine = `${indent}${last} size: ${body.size}`;
|
|
116
|
+
const responseBlock = [
|
|
117
|
+
`${pipe}`,
|
|
118
|
+
`${indent}response:`,
|
|
119
|
+
timeLine,
|
|
120
|
+
statusLine,
|
|
121
|
+
sizeLine,
|
|
122
|
+
].join("\n");
|
|
123
|
+
if (body.data === undefined) {
|
|
124
|
+
const head = label === undefined ? "" : `${label}\n`;
|
|
125
|
+
return [`${head}${responseBlock}`];
|
|
126
|
+
}
|
|
127
|
+
// Render the body using util.formatWithOptions so objects/arrays look like
|
|
128
|
+
// console.log output — colors, proper nesting, no JSON.stringify quirkiness.
|
|
129
|
+
const bodyText = (0, node_util_1.formatWithOptions)({ colors: useColor }, body.data);
|
|
130
|
+
const bodyLines = bodyText.split("\n");
|
|
131
|
+
const lastIdx = bodyLines.length - 1;
|
|
132
|
+
const bodyBlock = bodyLines
|
|
133
|
+
.map((line, i) => `${indent}${i === lastIdx ? last : branch} ${line}`)
|
|
134
|
+
.join("\n");
|
|
135
|
+
const full = [
|
|
136
|
+
`${pipe}`,
|
|
137
|
+
`${indent}response:`,
|
|
138
|
+
timeLine,
|
|
139
|
+
statusLine,
|
|
140
|
+
sizeLine,
|
|
141
|
+
`${pipe}`,
|
|
142
|
+
`${indent}body:`,
|
|
143
|
+
bodyBlock,
|
|
144
|
+
].join("\n");
|
|
145
|
+
const head = label === undefined ? "" : `${label}\n`;
|
|
146
|
+
return [`${head}${full}`];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The async tap. Fire-and-forget: returns `promise` immediately (unchanged),
|
|
150
|
+
* and logs a rich block once it resolves. For a `Response`, reads the body from
|
|
151
|
+
* a clone so the caller's original stays consumable. A rejection logs an error
|
|
152
|
+
* to stderr. `caller` MUST be resolved by `tap` (the user-facing function) so
|
|
153
|
+
* the logged location points at the user's file.
|
|
154
|
+
*/
|
|
155
|
+
function tapAsync(promise, label, caller) {
|
|
156
|
+
const useColor = (0, isTTY_js_1.isTTY)("stdout");
|
|
157
|
+
const start = node_perf_hooks_1.performance.now();
|
|
158
|
+
void promise.then((resolved) => {
|
|
159
|
+
const ms = Math.round(node_perf_hooks_1.performance.now() - start);
|
|
160
|
+
if (isResponse(resolved)) {
|
|
161
|
+
let clone = null;
|
|
162
|
+
try {
|
|
163
|
+
clone = resolved.clone();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
clone = null;
|
|
167
|
+
}
|
|
168
|
+
if (clone === null) {
|
|
169
|
+
(0, writeLog_js_1.writeLog)({
|
|
170
|
+
channel: "log", tag: "[TAP]",
|
|
171
|
+
args: buildBlock(label, ms, resolved, { data: undefined, size: "unknown" }, useColor),
|
|
172
|
+
caller,
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const cl = resolved.headers?.get?.("content-length");
|
|
177
|
+
const tooLarge = cl != null && cl !== "" && Number(cl) > 512 * 1024;
|
|
178
|
+
if (tooLarge) {
|
|
179
|
+
(0, writeLog_js_1.writeLog)({
|
|
180
|
+
channel: "log", tag: "[TAP]",
|
|
181
|
+
args: buildBlock(label, ms, resolved, { data: "(body too large to display)", size: formatBytes(Number(cl)) }, useColor),
|
|
182
|
+
caller,
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
void readBody(clone).then((body) => {
|
|
187
|
+
(0, writeLog_js_1.writeLog)({ channel: "log", tag: "[TAP]", args: buildBlock(label, ms, resolved, body, useColor), caller });
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Non-Response promise — plain value with elapsed time.
|
|
192
|
+
const elapsed = formatDuration(ms, useColor);
|
|
193
|
+
(0, writeLog_js_1.writeLog)({
|
|
194
|
+
channel: "log", tag: "[TAP]",
|
|
195
|
+
args: label === undefined ? [elapsed, resolved] : [`${label} ${elapsed}`, resolved],
|
|
196
|
+
caller,
|
|
197
|
+
});
|
|
198
|
+
}, (err) => {
|
|
199
|
+
const ms = Math.round(node_perf_hooks_1.performance.now() - start);
|
|
200
|
+
const elapsed = formatDuration(ms, useColor);
|
|
201
|
+
(0, writeLog_js_1.writeLog)({
|
|
202
|
+
channel: "error", tag: "[TAP]",
|
|
203
|
+
args: [label === undefined ? `rejected after ${elapsed}` : `${label} rejected after ${elapsed}`, err],
|
|
204
|
+
caller,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the given stream is an interactive terminal — the single source of
|
|
3
|
+
* truth for "should we emit terminal escapes (colors, clickable links)?". No env
|
|
4
|
+
* vars, no config. When output is piped or redirected this is `false`, so escape
|
|
5
|
+
* codes never end up in files or logs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isTTY(streamName: "stdout" | "stderr"): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* isTTY.ts
|
|
4
|
+
* Created by Dewan Mobashirul
|
|
5
|
+
* Copyright (c) 2026 dewan-meadown
|
|
6
|
+
* All rights reserved
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.isTTY = isTTY;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the given stream is an interactive terminal — the single source of
|
|
12
|
+
* truth for "should we emit terminal escapes (colors, clickable links)?". No env
|
|
13
|
+
* vars, no config. When output is piped or redirected this is `false`, so escape
|
|
14
|
+
* codes never end up in files or logs.
|
|
15
|
+
*/
|
|
16
|
+
function isTTY(streamName) {
|
|
17
|
+
if (typeof process === "undefined")
|
|
18
|
+
return false;
|
|
19
|
+
const stream = streamName === "stdout" ? process.stdout : process.stderr;
|
|
20
|
+
return Boolean(stream?.isTTY);
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** ANSI SGR codes for the colors/styles the logger uses. */
|
|
2
|
+
declare const CODES: {
|
|
3
|
+
readonly red: "38;2;239;68;68";
|
|
4
|
+
readonly yellow: 33;
|
|
5
|
+
readonly green: 32;
|
|
6
|
+
readonly cyan: "38;5;37";
|
|
7
|
+
readonly gray: 90;
|
|
8
|
+
readonly teal: "38;5;30";
|
|
9
|
+
readonly dimTeal: "38;5;23";
|
|
10
|
+
readonly bold: 1;
|
|
11
|
+
};
|
|
12
|
+
/** The named colors/styles {@link colorize} understands. */
|
|
13
|
+
export type Color = keyof typeof CODES;
|
|
14
|
+
/**
|
|
15
|
+
* Wraps `text` in the ANSI escape codes for `color`, resetting afterwards.
|
|
16
|
+
* Pure string work — deciding *whether* to colorize is the caller's job (see
|
|
17
|
+
* `isTTY`), kept separate so it can be checked once per entry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function colorize(text: string, color: Color): string;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* color.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*/
|
|
7
|
+
/** ANSI SGR codes for the colors/styles the logger uses. */
|
|
8
|
+
const CODES = {
|
|
9
|
+
red: "38;2;239;68;68", // truecolor #ef4444
|
|
10
|
+
yellow: 33,
|
|
11
|
+
green: 32,
|
|
12
|
+
cyan: "38;5;37", // 256-color deep cyan (#00afaf)
|
|
13
|
+
gray: 90, // bright black — renders as light gray
|
|
14
|
+
teal: "38;5;30", // 256-color teal (#008787)
|
|
15
|
+
dimTeal: "38;5;23", // 256-color darker teal (#005f5f)
|
|
16
|
+
bold: 1,
|
|
17
|
+
};
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
/**
|
|
20
|
+
* Wraps `text` in the ANSI escape codes for `color`, resetting afterwards.
|
|
21
|
+
* Pure string work — deciding *whether* to colorize is the caller's job (see
|
|
22
|
+
* `isTTY`), kept separate so it can be checked once per entry.
|
|
23
|
+
*/
|
|
24
|
+
export function colorize(text, color) {
|
|
25
|
+
return `\x1b[${CODES[color]}m${text}${RESET}`;
|
|
26
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Color } from "./colors/color.js";
|
|
2
|
+
/** The console channels the logger writes to. */
|
|
3
|
+
export type LogChannel = "log" | "error" | "warn";
|
|
4
|
+
/** The tag color per channel: info/tap → cyan, warn → yellow, error → red. */
|
|
5
|
+
export declare const TAG_COLOR: Record<LogChannel, Color>;
|
|
6
|
+
/** Glyphs that draw each entry's little tree. */
|
|
7
|
+
export declare const BRANCH = "\u251C\u2500\u2500";
|
|
8
|
+
export declare const BRANCH_END = "\u2514\u2500\u2500";
|
|
9
|
+
export declare const SEPARATOR = "-";
|
|
10
|
+
/** Hang-indent for message continuation lines, so they align under the message
|
|
11
|
+
* text (the `├── ` branch is 4 columns wide). */
|
|
12
|
+
export declare const MESSAGE_INDENT = "|\t";
|
|
13
|
+
/** Default for the collapse setting: 0 = show every line. */
|
|
14
|
+
export declare const DEFAULT_MAX_LINES = 0;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* constants.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*
|
|
7
|
+
* Single home for the logger's layout/behavior constants, so the values that
|
|
8
|
+
* shape an entry live in one place instead of as magic literals across modules.
|
|
9
|
+
*/
|
|
10
|
+
/** The tag color per channel: info/tap → cyan, warn → yellow, error → red. */
|
|
11
|
+
export const TAG_COLOR = {
|
|
12
|
+
log: "cyan",
|
|
13
|
+
warn: "yellow",
|
|
14
|
+
error: "red",
|
|
15
|
+
};
|
|
16
|
+
/** Glyphs that draw each entry's little tree. */
|
|
17
|
+
export const BRANCH = "├──"; // the message branch
|
|
18
|
+
export const BRANCH_END = "└──"; // the last (metadata) branch
|
|
19
|
+
export const SEPARATOR = "-"; // between the timestamp and the location
|
|
20
|
+
/** Hang-indent for message continuation lines, so they align under the message
|
|
21
|
+
* text (the `├── ` branch is 4 columns wide). */
|
|
22
|
+
export const MESSAGE_INDENT = "|\t";
|
|
23
|
+
/** Default for the collapse setting: 0 = show every line. */
|
|
24
|
+
export const DEFAULT_MAX_LINES = 0;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type LogChannel } from "../constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a log function bound to a console channel and tag. The returned closure
|
|
4
|
+
* is what the caller invokes directly, so {@link getCaller} resolves the caller's
|
|
5
|
+
* own frame; the resolved caller is then handed to {@link writeLog}, which never
|
|
6
|
+
* touches the stack. Logs only outside production — see {@link isLogAllowed}.
|
|
7
|
+
*/
|
|
8
|
+
export default function createLog(channel: LogChannel, tag: string): (...args: unknown[]) => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* createLog.ts
|
|
3
|
+
* Created by Dewan Mobashirul
|
|
4
|
+
* Copyright (c) 2026 dewan-meadown
|
|
5
|
+
* All rights reserved
|
|
6
|
+
*/
|
|
7
|
+
import getCaller from "../caller/getCaller.js";
|
|
8
|
+
import { writeLog } from "./writeLog.js";
|
|
9
|
+
import { isLogAllowed } from "../config.js";
|
|
10
|
+
/**
|
|
11
|
+
* Builds a log function bound to a console channel and tag. The returned closure
|
|
12
|
+
* is what the caller invokes directly, so {@link getCaller} resolves the caller's
|
|
13
|
+
* own frame; the resolved caller is then handed to {@link writeLog}, which never
|
|
14
|
+
* touches the stack. Logs only outside production — see {@link isLogAllowed}.
|
|
15
|
+
*/
|
|
16
|
+
export default function createLog(channel, tag) {
|
|
17
|
+
return (...args) => {
|
|
18
|
+
if (!isLogAllowed())
|
|
19
|
+
return;
|
|
20
|
+
const caller = getCaller();
|
|
21
|
+
writeLog({ channel, tag, args, caller });
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LogChannel } from "../constants.js";
|
|
2
|
+
import { type Caller } from "../caller/getCaller.js";
|
|
3
|
+
/** How many lines a long message shows before collapsing (0 = all). */
|
|
4
|
+
export declare function getVisibleLines(): number;
|
|
5
|
+
/** Set how many lines a long message shows before collapsing (0 = all). */
|
|
6
|
+
export declare function setVisibleLines(value: number): void;
|
|
7
|
+
/**
|
|
8
|
+
* Renders and writes one log entry. The `caller` is resolved by the *caller* of
|
|
9
|
+
* this function (the log closure or `tap`) and passed in, so this helper never
|
|
10
|
+
* touches the stack — keeping {@link getCaller}'s frame depth correct no matter
|
|
11
|
+
* which user-facing function delegates here.
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeLog(opts: {
|
|
14
|
+
channel: LogChannel;
|
|
15
|
+
tag: string;
|
|
16
|
+
args: unknown[];
|
|
17
|
+
caller: Caller;
|
|
18
|
+
}): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
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));
|
|
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 location = formatLocation(caller, useColor);
|
|
72
|
+
// Colors (terminal only): tag by level, timestamp teal, location dim teal,
|
|
73
|
+
// branch and separator gray.
|
|
74
|
+
const tagOut = useColor ? colorize(tag, TAG_COLOR[channel]) : tag;
|
|
75
|
+
const timeStamp = useColor ? colorize(getTimeStamp(), "teal") : getTimeStamp();
|
|
76
|
+
const locOut = useColor
|
|
77
|
+
? colorize(`(${location})`, "dimTeal")
|
|
78
|
+
: `(${location})`;
|
|
79
|
+
const connector = useColor ? colorize(BRANCH, "gray") : BRANCH;
|
|
80
|
+
const connectorBottom = useColor ? colorize(BRANCH_END, "gray") : BRANCH_END;
|
|
81
|
+
const separator = useColor ? colorize(SEPARATOR, "gray") : SEPARATOR;
|
|
82
|
+
// Layout: the tag, the message hanging off a `├──` branch, then the timestamp
|
|
83
|
+
// and location on a `└──` branch below. Leading `\n` spaces entries apart.
|
|
84
|
+
const message = renderMessage(args, useColor);
|
|
85
|
+
const meta = `\n${connectorBottom} ${timeStamp} ${separator} ${locOut}`;
|
|
86
|
+
console[channel](`\n${tagOut}`, `\n${connector}`, message, meta);
|
|
87
|
+
}
|
|
@@ -12,11 +12,3 @@ export declare function fileUrl(file: string): string;
|
|
|
12
12
|
* simply show `text`.
|
|
13
13
|
*/
|
|
14
14
|
export declare function hyperlink(text: string, url: string): string;
|
|
15
|
-
/**
|
|
16
|
-
* Whether to emit OSC-8 hyperlinks for the given stream. Driven solely by the
|
|
17
|
-
* stream being an interactive terminal (`isTTY`) — no env vars, no config. When
|
|
18
|
-
* output is piped or redirected, links are skipped so escapes never end up in
|
|
19
|
-
* files or logs. Terminals that don't understand OSC-8 just show the plain text.
|
|
20
|
-
*/
|
|
21
|
-
export declare function supportsHyperlinks(streamName: "stdout" | "stderr"): boolean;
|
|
22
|
-
//# sourceMappingURL=link.d.ts.map
|
|
@@ -25,16 +25,3 @@ export function hyperlink(text, url) {
|
|
|
25
25
|
const BEL = "\x07";
|
|
26
26
|
return `${OSC}${url}${BEL}${text}${OSC}${BEL}`;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Whether to emit OSC-8 hyperlinks for the given stream. Driven solely by the
|
|
30
|
-
* stream being an interactive terminal (`isTTY`) — no env vars, no config. When
|
|
31
|
-
* output is piped or redirected, links are skipped so escapes never end up in
|
|
32
|
-
* files or logs. Terminals that don't understand OSC-8 just show the plain text.
|
|
33
|
-
*/
|
|
34
|
-
export function supportsHyperlinks(streamName) {
|
|
35
|
-
if (typeof process === "undefined")
|
|
36
|
-
return false;
|
|
37
|
-
const stream = streamName === "stdout" ? process.stdout : process.stderr;
|
|
38
|
-
return Boolean(stream?.isTTY);
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=link.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
/** The logger: a callable for info logs, plus `.error
|
|
1
|
+
/** The logger: a callable for info logs, plus `.error`, `.warn`, and `.tap`. */
|
|
2
2
|
export interface LogFN {
|
|
3
3
|
(...args: unknown[]): void;
|
|
4
4
|
error(...args: unknown[]): void;
|
|
5
5
|
warn(...args: unknown[]): void;
|
|
6
|
+
/**
|
|
7
|
+
* Logs `value` (optionally tagged with `label`) and returns it **unchanged**,
|
|
8
|
+
* so it drops into any expression: `const u = logger.tap(getUser(), "user")`.
|
|
9
|
+
* Pass a **promise** (e.g. a `fetch`) and you get the same promise back while
|
|
10
|
+
* its elapsed time — and the HTTP status if it's a `Response` — is logged in
|
|
11
|
+
* the background. Silent in production; the value always flows through.
|
|
12
|
+
*/
|
|
13
|
+
tap<T>(value: T, label?: string): T;
|
|
6
14
|
/**
|
|
7
15
|
* How many lines of a multi-line message to show before collapsing the rest
|
|
8
16
|
* into a `… N more lines` summary. `0` (the default) shows everything.
|
|
@@ -22,4 +30,3 @@ export interface LogFN {
|
|
|
22
30
|
declare const logger: LogFN;
|
|
23
31
|
export { logger };
|
|
24
32
|
export default logger;
|
|
25
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Copyright (c) 2026 dewan-meadown
|
|
5
5
|
* All rights reserved
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import createLog from "./core/createLog.js";
|
|
8
|
+
import createTap from "./tap/createTap.js";
|
|
9
|
+
import { getVisibleLines, setVisibleLines } from "./core/writeLog.js";
|
|
8
10
|
/**
|
|
9
11
|
* Logs to the console, but only outside production. Each line is prefixed with
|
|
10
12
|
* a level tag, a short local timestamp, and a clickable link to the file it was
|
|
@@ -18,6 +20,7 @@ import { createLog, getVisibleLines, setVisibleLines } from "./utils/index.js";
|
|
|
18
20
|
const logger = Object.assign(createLog("log", "[INFO]"), {
|
|
19
21
|
error: createLog("error", "[ERROR]"),
|
|
20
22
|
warn: createLog("warn", "[WARN]"),
|
|
23
|
+
tap: createTap(),
|
|
21
24
|
});
|
|
22
25
|
// `maxLines` is a live getter/setter backed by the shared collapse setting, so
|
|
23
26
|
// setting it once affects info, error, and warn alike.
|
|
@@ -29,4 +32,3 @@ Object.defineProperty(logger, "maxLines", {
|
|
|
29
32
|
});
|
|
30
33
|
export { logger };
|
|
31
34
|
export default logger;
|
|
32
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Logs a value and returns it unchanged. Promises route to the timed path. */
|
|
2
|
+
export interface Tap {
|
|
3
|
+
<T>(value: T, label?: string): T;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Builds `tap` — logs a value and returns it **unchanged**, so it drops into any
|
|
7
|
+
* expression (`const u = logger.tap(getUser(), "user")`). The consumer always
|
|
8
|
+
* gets back exactly what they passed; the only effect is a clean log.
|
|
9
|
+
*
|
|
10
|
+
* - A plain value is logged synchronously and returned.
|
|
11
|
+
* - A **promise** is returned as-is (same object, never awaited or wrapped), and
|
|
12
|
+
* its elapsed time — plus the HTTP status if it resolves to a `Response` — is
|
|
13
|
+
* logged in the background (fire-and-forget, non-blocking).
|
|
14
|
+
*
|
|
15
|
+
* The returned closure is what the caller invokes directly, so {@link getCaller}
|
|
16
|
+
* resolves the caller's own frame. Silent in production; the value still flows.
|
|
17
|
+
*/
|
|
18
|
+
export default function createTap(): Tap;
|