@meadown/logger 1.8.10 → 1.9.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.
Files changed (142) hide show
  1. package/README.md +149 -51
  2. package/dist/cjs/{config.js → config/index.js} +1 -1
  3. package/dist/cjs/{constants.d.ts → const/index.d.ts} +0 -5
  4. package/dist/cjs/{constants.js → const/index.js} +2 -8
  5. package/dist/cjs/{caller → domain/caller}/getCaller.js +1 -1
  6. package/dist/cjs/{colors → domain/colors}/color.js +1 -1
  7. package/dist/cjs/{decorations → domain/decorations}/link.js +1 -1
  8. package/dist/cjs/{terminal → domain/terminal}/isTTY.js +1 -1
  9. package/dist/cjs/{time → domain/time}/getTimeStamp.js +1 -1
  10. package/dist/cjs/domain/write/helpers/buildContext.d.ts +16 -0
  11. package/dist/cjs/domain/write/helpers/buildContext.js +40 -0
  12. package/dist/cjs/{core/writeLog → domain/write/helpers}/formatLocation.js +3 -2
  13. package/dist/cjs/domain/write/helpers/index.d.ts +4 -0
  14. package/dist/cjs/domain/write/helpers/index.js +20 -0
  15. package/dist/cjs/{core/writeLog → domain/write/helpers}/renderMessage.js +6 -5
  16. package/dist/cjs/{core/writeLog → domain/write/helpers}/visibleLines.js +3 -3
  17. package/dist/cjs/domain/write/index.d.ts +2 -0
  18. package/dist/cjs/domain/write/index.js +14 -0
  19. package/dist/{core/writeLog/index.d.ts → cjs/domain/write/writeLog.d.ts} +2 -3
  20. package/dist/cjs/domain/write/writeLog.js +24 -0
  21. package/dist/cjs/features/logger/index.d.ts +1 -0
  22. package/dist/cjs/features/logger/index.js +21 -0
  23. package/dist/cjs/features/logger-error/index.d.ts +1 -0
  24. package/dist/cjs/features/logger-error/index.js +21 -0
  25. package/dist/cjs/features/logger-group/createGroup.d.ts +11 -0
  26. package/dist/cjs/features/logger-group/createGroup.js +21 -0
  27. package/dist/cjs/features/logger-group/index.d.ts +1 -0
  28. package/dist/cjs/features/logger-group/index.js +14 -0
  29. package/dist/cjs/features/logger-group/writeGroup.d.ts +8 -0
  30. package/dist/cjs/features/logger-group/writeGroup.js +20 -0
  31. package/dist/cjs/features/logger-max-lines/index.d.ts +1 -0
  32. package/dist/cjs/features/logger-max-lines/index.js +12 -0
  33. package/dist/cjs/features/logger-tap/createTap.d.ts +5 -0
  34. package/dist/cjs/features/logger-tap/createTap.js +33 -0
  35. package/dist/cjs/features/logger-tap/index.d.ts +1 -0
  36. package/dist/cjs/features/logger-tap/index.js +14 -0
  37. package/dist/cjs/features/logger-tap/tapAsync/helpers/buildBlock.d.ts +19 -0
  38. package/dist/cjs/features/logger-tap/tapAsync/helpers/buildBlock.js +56 -0
  39. package/dist/cjs/features/logger-tap/tapAsync/helpers/format.d.ts +4 -0
  40. package/dist/cjs/features/logger-tap/tapAsync/helpers/format.js +30 -0
  41. package/dist/cjs/features/logger-tap/tapAsync/helpers/index.d.ts +4 -0
  42. package/dist/cjs/features/logger-tap/tapAsync/helpers/index.js +20 -0
  43. package/dist/cjs/features/logger-tap/tapAsync/helpers/isThenable.d.ts +2 -0
  44. package/dist/cjs/features/logger-tap/tapAsync/helpers/isThenable.js +15 -0
  45. package/dist/cjs/features/logger-tap/tapAsync/helpers/response.d.ts +21 -0
  46. package/dist/cjs/features/logger-tap/tapAsync/helpers/response.js +62 -0
  47. package/dist/cjs/features/logger-tap/tapAsync/index.d.ts +2 -0
  48. package/dist/cjs/features/logger-tap/tapAsync/index.js +13 -0
  49. package/dist/cjs/{tap → features/logger-tap/tapAsync}/tapAsync.d.ts +1 -3
  50. package/dist/cjs/features/logger-tap/tapAsync/tapAsync.js +95 -0
  51. package/dist/cjs/features/logger-warn/index.d.ts +1 -0
  52. package/dist/cjs/features/logger-warn/index.js +21 -0
  53. package/dist/cjs/index.d.ts +63 -18
  54. package/dist/cjs/index.js +29 -15
  55. package/dist/cjs/types/index.d.ts +2 -0
  56. package/dist/cjs/types/index.js +8 -0
  57. package/dist/{config.js → config/index.js} +1 -1
  58. package/dist/{constants.d.ts → const/index.d.ts} +0 -5
  59. package/dist/{constants.js → const/index.js} +1 -7
  60. package/dist/{caller → domain/caller}/getCaller.js +1 -1
  61. package/dist/{colors → domain/colors}/color.js +1 -1
  62. package/dist/{decorations → domain/decorations}/link.js +1 -1
  63. package/dist/{terminal → domain/terminal}/isTTY.js +1 -1
  64. package/dist/{time → domain/time}/getTimeStamp.js +1 -1
  65. package/dist/domain/write/helpers/buildContext.d.ts +16 -0
  66. package/dist/domain/write/helpers/buildContext.js +33 -0
  67. package/dist/{core/writeLog → domain/write/helpers}/formatLocation.js +3 -2
  68. package/dist/domain/write/helpers/index.d.ts +4 -0
  69. package/dist/domain/write/helpers/index.js +10 -0
  70. package/dist/{core/writeLog → domain/write/helpers}/renderMessage.js +5 -4
  71. package/dist/{core/writeLog → domain/write/helpers}/visibleLines.js +2 -2
  72. package/dist/domain/write/index.d.ts +2 -0
  73. package/dist/domain/write/index.js +8 -0
  74. package/dist/{cjs/core/writeLog/index.d.ts → domain/write/writeLog.d.ts} +2 -3
  75. package/dist/domain/write/writeLog.js +21 -0
  76. package/dist/features/logger/index.d.ts +1 -0
  77. package/dist/features/logger/index.js +15 -0
  78. package/dist/features/logger-error/index.d.ts +1 -0
  79. package/dist/features/logger-error/index.js +15 -0
  80. package/dist/features/logger-group/createGroup.d.ts +11 -0
  81. package/dist/features/logger-group/createGroup.js +15 -0
  82. package/dist/features/logger-group/index.d.ts +1 -0
  83. package/dist/features/logger-group/index.js +7 -0
  84. package/dist/features/logger-group/writeGroup.d.ts +8 -0
  85. package/dist/features/logger-group/writeGroup.js +17 -0
  86. package/dist/features/logger-max-lines/index.d.ts +1 -0
  87. package/dist/features/logger-max-lines/index.js +7 -0
  88. package/dist/features/logger-tap/createTap.d.ts +5 -0
  89. package/dist/features/logger-tap/createTap.js +27 -0
  90. package/dist/features/logger-tap/index.d.ts +1 -0
  91. package/dist/features/logger-tap/index.js +7 -0
  92. package/dist/features/logger-tap/tapAsync/helpers/buildBlock.d.ts +19 -0
  93. package/dist/features/logger-tap/tapAsync/helpers/buildBlock.js +53 -0
  94. package/dist/features/logger-tap/tapAsync/helpers/format.d.ts +4 -0
  95. package/dist/features/logger-tap/tapAsync/helpers/format.js +26 -0
  96. package/dist/features/logger-tap/tapAsync/helpers/index.d.ts +4 -0
  97. package/dist/features/logger-tap/tapAsync/helpers/index.js +10 -0
  98. package/dist/features/logger-tap/tapAsync/helpers/isThenable.d.ts +2 -0
  99. package/dist/features/logger-tap/tapAsync/helpers/isThenable.js +12 -0
  100. package/dist/features/logger-tap/tapAsync/helpers/response.d.ts +21 -0
  101. package/dist/features/logger-tap/tapAsync/helpers/response.js +57 -0
  102. package/dist/features/logger-tap/tapAsync/index.d.ts +2 -0
  103. package/dist/features/logger-tap/tapAsync/index.js +8 -0
  104. package/dist/{tap → features/logger-tap/tapAsync}/tapAsync.d.ts +1 -3
  105. package/dist/features/logger-tap/tapAsync/tapAsync.js +92 -0
  106. package/dist/features/logger-warn/index.d.ts +1 -0
  107. package/dist/features/logger-warn/index.js +15 -0
  108. package/dist/index.d.ts +63 -18
  109. package/dist/index.js +29 -15
  110. package/dist/types/index.d.ts +2 -0
  111. package/dist/types/index.js +7 -0
  112. package/package.json +7 -4
  113. package/dist/cjs/core/createLog.d.ts +0 -8
  114. package/dist/cjs/core/createLog.js +0 -29
  115. package/dist/cjs/core/writeLog/index.js +0 -48
  116. package/dist/cjs/tap/createTap.d.ts +0 -18
  117. package/dist/cjs/tap/createTap.js +0 -51
  118. package/dist/cjs/tap/tapAsync.js +0 -192
  119. package/dist/core/createLog.d.ts +0 -8
  120. package/dist/core/createLog.js +0 -23
  121. package/dist/core/writeLog/index.js +0 -39
  122. package/dist/tap/createTap.d.ts +0 -18
  123. package/dist/tap/createTap.js +0 -45
  124. package/dist/tap/tapAsync.js +0 -188
  125. /package/dist/cjs/{config.d.ts → config/index.d.ts} +0 -0
  126. /package/dist/{caller → cjs/domain/caller}/getCaller.d.ts +0 -0
  127. /package/dist/cjs/{colors → domain/colors}/color.d.ts +0 -0
  128. /package/dist/cjs/{decorations → domain/decorations}/link.d.ts +0 -0
  129. /package/dist/cjs/{terminal → domain/terminal}/isTTY.d.ts +0 -0
  130. /package/dist/cjs/{time → domain/time}/getTimeStamp.d.ts +0 -0
  131. /package/dist/cjs/{core/writeLog → domain/write/helpers}/formatLocation.d.ts +0 -0
  132. /package/dist/cjs/{core/writeLog → domain/write/helpers}/renderMessage.d.ts +0 -0
  133. /package/dist/cjs/{core/writeLog → domain/write/helpers}/visibleLines.d.ts +0 -0
  134. /package/dist/{config.d.ts → config/index.d.ts} +0 -0
  135. /package/dist/{cjs → domain}/caller/getCaller.d.ts +0 -0
  136. /package/dist/{colors → domain/colors}/color.d.ts +0 -0
  137. /package/dist/{decorations → domain/decorations}/link.d.ts +0 -0
  138. /package/dist/{terminal → domain/terminal}/isTTY.d.ts +0 -0
  139. /package/dist/{time → domain/time}/getTimeStamp.d.ts +0 -0
  140. /package/dist/{core/writeLog → domain/write/helpers}/formatLocation.d.ts +0 -0
  141. /package/dist/{core/writeLog → domain/write/helpers}/renderMessage.d.ts +0 -0
  142. /package/dist/{core/writeLog → domain/write/helpers}/visibleLines.d.ts +0 -0
@@ -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 index_js_1 = require("../core/writeLog/index.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, index_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, index_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, index_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, index_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, index_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
- }
@@ -1,8 +0,0 @@
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;
@@ -1,23 +0,0 @@
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/index.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
- }
@@ -1,39 +0,0 @@
1
- /*
2
- * index.ts
3
- * Created by Dewan Mobashirul
4
- * Copyright (c) 2026 dewan-meadown
5
- * All rights reserved
6
- */
7
- import { TAG_COLOR, BRANCH, BRANCH_END, SEPARATOR, } from "../../constants.js";
8
- import getTimeStamp from "../../time/getTimeStamp.js";
9
- import { colorize } from "../../colors/color.js";
10
- import { isTTY } from "../../terminal/isTTY.js";
11
- import { renderMessage } from "./renderMessage.js";
12
- import { formatLocation } from "./formatLocation.js";
13
- export { getVisibleLines, setVisibleLines } from "./visibleLines.js";
14
- /**
15
- * Renders and writes one log entry. The `caller` is resolved by the *caller* of
16
- * this function (the log closure or `tap`) and passed in, so this helper never
17
- * touches the stack — keeping {@link getCaller}'s frame depth correct no matter
18
- * which user-facing function delegates here.
19
- */
20
- export function writeLog(opts) {
21
- const { channel, tag, args, caller } = opts;
22
- const streamName = channel === "log" ? "stdout" : "stderr";
23
- // One terminal check drives both color and clickable links — `isTTY` is the
24
- // single source of truth (DRY). Off when output is piped/redirected.
25
- const useColor = isTTY(streamName);
26
- const paint = (s, c) => useColor ? colorize(s, c) : s;
27
- const location = formatLocation(caller, useColor);
28
- const tagOut = paint(tag, TAG_COLOR[channel]);
29
- const timeStamp = paint(getTimeStamp(), "teal");
30
- const locOut = paint(`(${location})`, "dimTeal");
31
- const connector = paint(BRANCH, "gray");
32
- const connectorBottom = paint(BRANCH_END, "gray");
33
- const separator = paint(SEPARATOR, "gray");
34
- // Layout: the tag, the message hanging off a `├──` branch, then the timestamp
35
- // and location on a `└──` branch below. Leading `\n` spaces entries apart.
36
- const message = renderMessage(args, useColor);
37
- const meta = `\n${connectorBottom} ${timeStamp} ${separator} ${locOut}`;
38
- console[channel](`\n${tagOut}`, `\n${connector}`, message, meta);
39
- }
@@ -1,18 +0,0 @@
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;
@@ -1,45 +0,0 @@
1
- /*
2
- * createTap.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 "../core/writeLog/index.js";
9
- import { isLogAllowed } from "../config.js";
10
- import { isThenable, tapAsync } from "./tapAsync.js";
11
- /**
12
- * Builds `tap` — logs a value and returns it **unchanged**, so it drops into any
13
- * expression (`const u = logger.tap(getUser(), "user")`). The consumer always
14
- * gets back exactly what they passed; the only effect is a clean log.
15
- *
16
- * - A plain value is logged synchronously and returned.
17
- * - A **promise** is returned as-is (same object, never awaited or wrapped), and
18
- * its elapsed time — plus the HTTP status if it resolves to a `Response` — is
19
- * logged in the background (fire-and-forget, non-blocking).
20
- *
21
- * The returned closure is what the caller invokes directly, so {@link getCaller}
22
- * resolves the caller's own frame. Silent in production; the value still flows.
23
- */
24
- export default function createTap() {
25
- const tap = (value, label) => {
26
- if (!isLogAllowed())
27
- return value;
28
- const caller = getCaller();
29
- if (isThenable(value)) {
30
- // value is a promise → hand off to the async tap (caller passed in so the
31
- // location stays on the user's file, not on this helper).
32
- tapAsync(value, label, caller);
33
- }
34
- else {
35
- writeLog({
36
- channel: "log",
37
- tag: "[TAP]",
38
- args: label === undefined ? [value] : [label, value],
39
- caller,
40
- });
41
- }
42
- return value;
43
- };
44
- return tap;
45
- }
@@ -1,188 +0,0 @@
1
- /*
2
- * tapAsync.ts
3
- * Created by Dewan Mobashirul
4
- * Copyright (c) 2026 dewan-meadown
5
- * All rights reserved
6
- *
7
- * The async side of `tap`: times a promise and logs a rich response block when
8
- * it resolves to a `Response` — status color, slow-request highlighting, size,
9
- * and the actual body — all fire-and-forget so the caller never waits.
10
- */
11
- import { performance } from "node:perf_hooks";
12
- import { formatWithOptions } from "node:util";
13
- import { writeLog } from "../core/writeLog/index.js";
14
- import { colorize } from "../colors/color.js";
15
- import { isTTY } from "../terminal/isTTY.js";
16
- /** Whether `value` is thenable (a promise we can await + time). */
17
- export function isThenable(value) {
18
- return (typeof value === "object" &&
19
- value !== null &&
20
- typeof value.then === "function");
21
- }
22
- /** Whether the resolved value is a fetch `Response` we can read. */
23
- function isResponse(value) {
24
- const v = value;
25
- return (typeof v?.status === "number" &&
26
- typeof v.clone === "function" &&
27
- typeof v.text === "function");
28
- }
29
- /** `65ms` (green) · `1.2s` (yellow) · `5.8s` (red). */
30
- function formatDuration(ms, useColor) {
31
- const text = ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
32
- if (!useColor)
33
- return text;
34
- if (ms >= 2000)
35
- return colorize(text, "red");
36
- if (ms >= 500)
37
- return colorize(text, "yellow");
38
- return colorize(text, "green");
39
- }
40
- /** `848 B` / `1.84 KB` / `2.10 MB`. */
41
- function formatBytes(bytes) {
42
- if (bytes < 1024)
43
- return `${bytes} B`;
44
- if (bytes < 1024 * 1024)
45
- return `${(bytes / 1024).toFixed(2)} KB`;
46
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
47
- }
48
- /**
49
- * Status badge color:
50
- * 2xx → green · 3xx → cyan · 4xx → yellow · 5xx → red
51
- */
52
- function statusColor(status) {
53
- if (status >= 500)
54
- return "red";
55
- if (status >= 400)
56
- return "yellow";
57
- if (status >= 300)
58
- return "cyan";
59
- return "green";
60
- }
61
- function formatStatus(res, useColor) {
62
- const text = typeof res.statusText === "string" && res.statusText
63
- ? `${res.status} ${res.statusText}`
64
- : `${res.status}`;
65
- return useColor ? colorize(text, statusColor(res.status)) : text;
66
- }
67
- /**
68
- * Reads a (cloned) response body and returns both the parsed value and the
69
- * actual byte size. Size is calculated from the body text — not `Content-Length`,
70
- * which is absent on compressed responses — so size is always shown.
71
- */
72
- async function readBody(res) {
73
- try {
74
- const text = await res.text();
75
- const bytes = new TextEncoder().encode(text).length;
76
- const size = formatBytes(bytes);
77
- if (text === "")
78
- return { data: undefined, size };
79
- try {
80
- return { data: JSON.parse(text), size };
81
- }
82
- catch {
83
- return { data: text, size };
84
- }
85
- }
86
- catch {
87
- return { data: undefined, size: "unknown" };
88
- }
89
- }
90
- /**
91
- * Renders the nested tree block the user asked for:
92
- *
93
- * GET /users/1
94
- * │
95
- * │ response:
96
- * │ ├── time: 65ms
97
- * │ ├── status: 200 OK
98
- * │ └── size: 848 B
99
- * │
100
- * │ body:
101
- * │ ├── id: 1
102
- * │ └── name: Leanne Graham
103
- */
104
- function buildBlock(label, ms, res, body, useColor) {
105
- const paint = (s, c) => useColor ? colorize(s, c) : s;
106
- const pipe = paint("│", "gray");
107
- const branch = paint("├──", "gray");
108
- const last = paint("└──", "gray");
109
- const indent = `${pipe} `;
110
- const timeLine = `${indent}${branch} time: ${formatDuration(ms, useColor)}`;
111
- const statusLine = `${indent}${branch} status: ${formatStatus(res, useColor)}`;
112
- const sizeLine = `${indent}${last} size: ${body.size}`;
113
- const responseLines = [`${pipe}`, `${indent}response:`, timeLine, statusLine, sizeLine];
114
- const head = label === undefined ? "" : `${label}\n`;
115
- if (body.data === undefined) {
116
- return [`${head}${responseLines.join("\n")}`];
117
- }
118
- // Render the body using util.formatWithOptions so objects/arrays look like
119
- // console.log output — colors, proper nesting, no JSON.stringify quirkiness.
120
- const bodyText = formatWithOptions({ colors: useColor }, body.data);
121
- const bodyLines = bodyText.split("\n");
122
- const lastIdx = bodyLines.length - 1;
123
- const bodyBlock = bodyLines
124
- .map((line, i) => `${indent}${i === lastIdx ? last : branch} ${line}`)
125
- .join("\n");
126
- const full = [...responseLines, `${pipe}`, `${indent}body:`, bodyBlock].join("\n");
127
- return [`${head}${full}`];
128
- }
129
- /**
130
- * The async tap. Fire-and-forget: returns `promise` immediately (unchanged),
131
- * and logs a rich block once it resolves. For a `Response`, reads the body from
132
- * a clone so the caller's original stays consumable. A rejection logs an error
133
- * to stderr. `caller` MUST be resolved by `tap` (the user-facing function) so
134
- * the logged location points at the user's file.
135
- */
136
- export function tapAsync(promise, label, caller) {
137
- const useColor = isTTY("stdout");
138
- const start = performance.now();
139
- void promise.then((resolved) => {
140
- const ms = Math.round(performance.now() - start);
141
- if (isResponse(resolved)) {
142
- let clone = null;
143
- try {
144
- clone = resolved.clone();
145
- }
146
- catch {
147
- clone = null;
148
- }
149
- if (clone === null) {
150
- writeLog({
151
- channel: "log", tag: "[TAP]",
152
- args: buildBlock(label, ms, resolved, { data: undefined, size: "unknown" }, useColor),
153
- caller,
154
- });
155
- return;
156
- }
157
- const cl = resolved.headers?.get?.("content-length");
158
- const tooLarge = cl != null && cl !== "" && Number(cl) > 512 * 1024;
159
- if (tooLarge) {
160
- writeLog({
161
- channel: "log", tag: "[TAP]",
162
- args: buildBlock(label, ms, resolved, { data: "(body too large to display)", size: formatBytes(Number(cl)) }, useColor),
163
- caller,
164
- });
165
- return;
166
- }
167
- void readBody(clone).then((body) => {
168
- writeLog({ channel: "log", tag: "[TAP]", args: buildBlock(label, ms, resolved, body, useColor), caller });
169
- });
170
- return;
171
- }
172
- // Non-Response promise — plain value with elapsed time.
173
- const elapsed = formatDuration(ms, useColor);
174
- writeLog({
175
- channel: "log", tag: "[TAP]",
176
- args: label === undefined ? [elapsed, resolved] : [`${label} ${elapsed}`, resolved],
177
- caller,
178
- });
179
- }, (err) => {
180
- const ms = Math.round(performance.now() - start);
181
- const elapsed = formatDuration(ms, useColor);
182
- writeLog({
183
- channel: "error", tag: "[TAP]",
184
- args: [label === undefined ? `rejected after ${elapsed}` : `${label} rejected after ${elapsed}`, err],
185
- caller,
186
- });
187
- });
188
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes