@meadown/logger 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +95 -63
  2. package/SECURITY.md +30 -4
  3. package/dist/{utils → caller}/getCaller.js +0 -1
  4. package/dist/{utils → cjs/caller}/getCaller.d.ts +0 -1
  5. package/dist/cjs/colors/color.d.ts +20 -0
  6. package/dist/cjs/colors/color.js +29 -0
  7. package/dist/cjs/constants.d.ts +14 -0
  8. package/dist/cjs/constants.js +27 -0
  9. package/dist/cjs/core/createLog.d.ts +8 -0
  10. package/dist/cjs/core/createLog.js +29 -0
  11. package/dist/cjs/core/writeLog.d.ts +18 -0
  12. package/dist/cjs/core/writeLog.js +95 -0
  13. package/dist/cjs/{utils → decorations}/link.d.ts +0 -7
  14. package/dist/cjs/{utils → decorations}/link.js +0 -13
  15. package/dist/cjs/index.d.ts +9 -1
  16. package/dist/cjs/index.js +12 -6
  17. package/dist/cjs/tap/createTap.d.ts +18 -0
  18. package/dist/cjs/tap/createTap.js +51 -0
  19. package/dist/cjs/tap/tapAsync.d.ts +11 -0
  20. package/dist/cjs/tap/tapAsync.js +207 -0
  21. package/dist/cjs/terminal/isTTY.d.ts +7 -0
  22. package/dist/cjs/terminal/isTTY.js +21 -0
  23. package/dist/colors/color.d.ts +20 -0
  24. package/dist/colors/color.js +26 -0
  25. package/dist/config.d.ts +0 -1
  26. package/dist/config.js +0 -1
  27. package/dist/constants.d.ts +14 -0
  28. package/dist/constants.js +24 -0
  29. package/dist/core/createLog.d.ts +8 -0
  30. package/dist/core/createLog.js +23 -0
  31. package/dist/core/writeLog.d.ts +18 -0
  32. package/dist/core/writeLog.js +87 -0
  33. package/dist/{utils → decorations}/link.d.ts +0 -8
  34. package/dist/{utils → decorations}/link.js +0 -13
  35. package/dist/index.d.ts +9 -2
  36. package/dist/index.js +4 -2
  37. package/dist/tap/createTap.d.ts +18 -0
  38. package/dist/tap/createTap.js +45 -0
  39. package/dist/tap/tapAsync.d.ts +11 -0
  40. package/dist/tap/tapAsync.js +203 -0
  41. package/dist/terminal/isTTY.d.ts +7 -0
  42. package/dist/terminal/isTTY.js +18 -0
  43. package/dist/{utils → time}/getTimeStamp.d.ts +0 -1
  44. package/dist/{utils → time}/getTimeStamp.js +0 -1
  45. package/package.json +4 -2
  46. package/dist/cjs/utils/color.d.ts +0 -25
  47. package/dist/cjs/utils/color.js +0 -40
  48. package/dist/cjs/utils/createLog.d.ts +0 -14
  49. package/dist/cjs/utils/createLog.js +0 -116
  50. package/dist/cjs/utils/index.d.ts +0 -5
  51. package/dist/cjs/utils/index.js +0 -27
  52. package/dist/config.d.ts.map +0 -1
  53. package/dist/config.js.map +0 -1
  54. package/dist/index.d.ts.map +0 -1
  55. package/dist/index.js.map +0 -1
  56. package/dist/utils/color.d.ts +0 -26
  57. package/dist/utils/color.d.ts.map +0 -1
  58. package/dist/utils/color.js +0 -37
  59. package/dist/utils/color.js.map +0 -1
  60. package/dist/utils/createLog.d.ts +0 -15
  61. package/dist/utils/createLog.d.ts.map +0 -1
  62. package/dist/utils/createLog.js +0 -109
  63. package/dist/utils/createLog.js.map +0 -1
  64. package/dist/utils/getCaller.d.ts.map +0 -1
  65. package/dist/utils/getCaller.js.map +0 -1
  66. package/dist/utils/getTimeStamp.d.ts.map +0 -1
  67. package/dist/utils/getTimeStamp.js.map +0 -1
  68. package/dist/utils/index.d.ts +0 -6
  69. package/dist/utils/index.d.ts.map +0 -1
  70. package/dist/utils/index.js +0 -12
  71. package/dist/utils/index.js.map +0 -1
  72. package/dist/utils/link.d.ts.map +0 -1
  73. package/dist/utils/link.js.map +0 -1
  74. /package/dist/{cjs/utils → caller}/getCaller.d.ts +0 -0
  75. /package/dist/cjs/{utils → caller}/getCaller.js +0 -0
  76. /package/dist/cjs/{utils → time}/getTimeStamp.d.ts +0 -0
  77. /package/dist/cjs/{utils → time}/getTimeStamp.js +0 -0
package/README.md CHANGED
@@ -1,16 +1,32 @@
1
1
  # @meadown/logger
2
2
 
3
- I kept writing `console.log` everywhere, then squinting at my terminal trying to
4
- remember _which file_ a message came from — and worse, forgetting to pull those logs
5
- out before shipping. So I made this.
6
-
7
- It's basically `console.log` with the rough edges sanded off: every message gets a
8
- **color-coded** level tag, a timestamp, and the file and line it came from — as a
9
- **clickable link** you can open straight from your terminal. And it stays quiet in
10
- production, so you can leave your logs where they are and not worry about them.
3
+ A **development-focused logger** for Node.js and TypeScript built to make your
4
+ development loop faster and your terminal actually readable.
11
5
 
12
6
  No dependencies. No config. Import it and you're done.
13
7
 
8
+ ## Why this exists
9
+
10
+ I kept writing the same custom log wrapper in every project — the one that
11
+ silences itself in production and shows a timestamp. I kept forgetting to use
12
+ it, shipping stray `console.log` calls, and having no idea which file a log
13
+ message came from. So I built the version I always wanted: zero dependencies,
14
+ automatic production silence, and every line tells you exactly where it came
15
+ from as a clickable link.
16
+
17
+ > Full story — problem, research, design, build, and what got cut along the
18
+ > way — in [`docs/STORY.md`](docs/STORY.md).
19
+
20
+ ## Features
21
+
22
+ - **Zero dependencies**
23
+ - **Development-focused** — built for the dev experience, not production ops
24
+ - **Clickable source link** — every log is a clickable link to the exact file it came from
25
+ - **API response logging** — `tap` a fetch and get timing, status, size, and body automatically
26
+ - **Color-coded levels** — `[INFO]` cyan, `[WARN]` yellow, `[ERROR]` red
27
+ - **Tree layout output** — clean, scannable structure in your terminal
28
+ - **Collapsible messages** — cap long output with `logger.maxLines`
29
+
14
30
  ## Install
15
31
 
16
32
  ```bash
@@ -27,100 +43,116 @@ yarn add @meadown/logger
27
43
  import logger from "@meadown/logger"
28
44
 
29
45
  logger("Hello world")
30
- logger("Auth", "user logged in") // every argument is printed as-is, like console.log
46
+ logger("Auth", "user logged in")
31
47
 
32
48
  logger.warn("This is deprecated")
33
49
  logger.error("Something went wrong")
34
50
  ```
35
51
 
36
- You'll see something like:
37
-
38
52
  ```text
39
53
  [INFO]
40
54
  ├── Auth user logged in
41
55
  └── 05-30 04:00:00 PM - (server.ts:42)
42
56
  ```
43
57
 
44
- Each entry is a little tree: the level tag — `[INFO]`, `[WARN]`, or `[ERROR]` — on
45
- top, your message hanging off a `├──` branch, and a short local timestamp
46
- (month-day, 12-hour time) plus the source location on the `└──` branch below.
47
- Entries are spaced apart by a blank line so they're easy to scan in a busy terminal.
48
-
49
- ### One thing if you re-export it
58
+ ## API response logging
50
59
 
51
- A lot of projects like to funnel everything through their own `lib/logger` file.
52
- That's totally fine here just pass the logger straight through instead of wrapping
53
- it in a new function. The file and line are read from the call stack, so an extra
54
- wrapper makes every log blame _that_ file instead of wherever you actually logged.
60
+ Drop `tap` into any `await` chain you get timing, status, size, and the
61
+ actual response body. The promise flows through untouched. One line of code.
55
62
 
56
63
  ```ts
57
- // GOOD: pass it through — the (file:line) stays honest
58
- export { default as logger } from "@meadown/logger"
64
+ const user = await logger.tap(
65
+ fetch("https://api.example.com/users/1"),
66
+ "GET /users/1"
67
+ )
68
+ // user is the real Response — your code doesn't change at all
69
+ ```
59
70
 
60
- // BAD: now every log points at this file, not the real caller
61
- export const logger = (...args) => log(...args)
71
+ ```text
72
+ [TAP]
73
+ ├── GET /users/1
74
+
75
+ │ response:
76
+ │ ├── time: 65ms
77
+ │ ├── status: 200 OK
78
+ │ └── size: 848 B
79
+
80
+ │ body:
81
+ │ ├── id: 1
82
+ │ ├── name: Leanne Graham
83
+ │ └── email: Sincere@april.biz
84
+
85
+ └── 05-30 07:54:26 PM - (api.ts:12)
86
+ ```
87
+
88
+ You can immediately see: was it successful? How long did it take? What came
89
+ back? Without opening DevTools.
90
+
91
+ ![API response logging — tap a fetch and see timing, status, size, and body](media/tap-api-demo.png)
92
+
93
+ Works with plain values too — logs it, returns it, nothing changes:
94
+
95
+ ```ts
96
+ const port = logger.tap(5000, "port") // port is still 5000
97
+ const user = logger.tap(await getUser(), "user") // same as without tap
62
98
  ```
63
99
 
100
+ ## Clickable source link
101
+
102
+ That `(server.ts:42)` is a **clickable link** — open it and you land on the exact
103
+ line that wrote the log. Works in VS Code, iTerm2, WezTerm, Kitty, and Windows
104
+ Terminal. Degrades to plain text everywhere else.
105
+
64
106
  ## Color-coded levels
65
107
 
66
- The level tag is colored so you can spot what matters at a glance — `[INFO]` in
67
- cyan, `[WARN]` in yellow, `[ERROR]` in red. The timestamp and source location are
68
- tinted teal, and the tree branches sit in a quiet gray, so the colored level tag is
69
- what your eye lands on first.
108
+ `[INFO]` cyan · `[WARN]` yellow · `[ERROR]` red. Timestamp and location tinted teal.
109
+ Auto-disabled when output is piped no escape codes in your log files.
70
110
 
71
- Colors appear automatically when you're in a terminal. When output is piped to a
72
- file or another program, everything prints as plain text — no stray color codes in
73
- your log files. Nothing to configure.
111
+ ## Tree layout output
74
112
 
75
- ## Click to open the source
113
+ ```text
114
+ [INFO]
115
+ ├── Auth user logged in
116
+ └── 05-30 04:00:00 PM - (server.ts:42)
117
+ ```
76
118
 
77
- That `(server.ts:42)` at the end of every log isn't just text when you are in a
78
- terminal, it's a **clickable link** that opens the file the log came from. No more
79
- hunting for where a message came from, and the line number is right there in the
80
- label.
119
+ Level tag, message, timestamp, and location all in a clean tree. Easy to scan,
120
+ even in a busy terminal.
81
121
 
82
- There's nothing to configure. Links show up automatically when output goes to a
83
- terminal, and when it's piped to a file or another program they quietly drop to
84
- plain `(server.ts:42)` text — so your log files never get cluttered with escape
85
- codes.
122
+ ## Collapsible messages
86
123
 
87
- ## Trimming long messages
124
+ ```ts
125
+ logger.maxLines = 5 // show 5 lines, then "... N more lines"
126
+ logger.maxLines = 0 // default — show everything
127
+ ```
88
128
 
89
- By default you see everything you log, however long. But if a big object or a
90
- chatty multi-line message is drowning out your terminal, you can cap how many
91
- lines each message shows:
129
+ ### One thing if you re-export it
92
130
 
93
131
  ```ts
94
- import logger from "@meadown/logger"
132
+ // GOOD location stays honest
133
+ export { default as logger } from "@meadown/logger"
95
134
 
96
- logger.maxLines = 5 // show the first 5 lines, then "... N more lines"
97
- logger.maxLines = 0 // back to the default — show everything
135
+ // BAD every log points at this file, not the real caller
136
+ export const logger = (...args) => log(...args)
98
137
  ```
99
138
 
100
- It only trims the _message_, never the tag, timestamp, or location, and the setting
101
- applies to `logger`, `.error`, and `.warn` alike.
139
+ ## NODE_ENV
102
140
 
103
- ## What about production?
104
-
105
- Here's the nice part: you don't have to do anything. Logs show up while you're
106
- developing and go silent in production. The only thing that flips the switch is your
107
- `NODE_ENV`:
141
+ This logger reads `NODE_ENV` to decide when to log. By default it logs everywhere
142
+ except `production`. This is a deliberate dev-focused default — a production-grade
143
+ logger with transport, persistence, and log levels is a separate concern and a
144
+ future direction.
108
145
 
109
146
  | `NODE_ENV` | Logs? |
110
147
  | ---------------------------------------- | ------ |
111
148
  | not set, `development`, or anything else | shown |
112
149
  | `production` | silent |
113
150
 
114
- So leave your logs in the code. Once you ship with `NODE_ENV=production`, they just
115
- quietly step aside.
116
-
117
151
  ## Security
118
152
 
119
- It's a tiny, zero-dependency package with no file, network, or dynamic-code access.
120
- See [SECURITY.md](https://www.npmjs.com/package/@meadown/logger?activeTab=code)
121
- for the security model and how to report a vulnerability. One thing to know: like
122
- `console.log`, log arguments are written to the terminal as-is and are not
123
- sanitized — don't log untrusted data to a terminal you trust.
153
+ Zero dependencies, no file or network access, nothing persisted.
154
+ See [SECURITY.md](https://github.com/meadown/meadown-logger/blob/main/SECURITY.md)
155
+ for the full security model.
124
156
 
125
157
  ## License
126
158
 
package/SECURITY.md CHANGED
@@ -23,14 +23,37 @@ Only the latest published `@meadown/logger` release receives security fixes.
23
23
  packages, so there is no third-party supply-chain surface to inherit.
24
24
  - **No I/O or dynamic execution.** It does not read or write files, open network
25
25
  connections, spawn processes, or use `eval`/`Function`. It only writes to the
26
- console and reads `process.env.NODE_ENV` (to stay quiet in production).
26
+ console and reads `process.env.NODE_ENV` (to stay quiet in production). (The
27
+ `examples/` directory is not part of the published package and may make real
28
+ network calls when you run it.)
27
29
  - **Nothing is persisted.** Log output is never written to disk, so the logger
28
30
  cannot leak logged data to a temp file or similar.
29
31
 
30
- ## Trust boundary: log arguments are output, not input
32
+ ## What the logger does with your values
31
33
 
32
- Like `console.log` itself, this logger passes the arguments you give it through
33
- to the terminal as output. **It does not sanitize them.**
34
+ Nothing beyond showing them to you. This matters most for `tap`, which is built to
35
+ log values like API responses that often carry tokens, sessions, or personal data.
36
+ The logger does **not**:
37
+
38
+ - **store or persist** any value — no files, no database, no caching, no buffering;
39
+ nothing is kept after the log call returns;
40
+ - **send anything over the network** — there is no networking code and no
41
+ dependencies, so there is nothing that could "phone home";
42
+ - **read, copy, or forward** tokens, cookies, session IDs, or credentials, and it
43
+ does not hijack or inspect sessions;
44
+ - **mutate** what you pass — `tap` returns the exact same reference, unchanged.
45
+
46
+ `logger.tap(await login(), "auth")` writes the response to your terminal and hands
47
+ it straight back. The value never leaves your process — the only "exposure" is the
48
+ text printed to your own console (see the trust boundary below).
49
+
50
+ ## Trust boundary: logged values are output, not input
51
+
52
+ Like `console.log` itself, this logger passes whatever you give it through to the
53
+ terminal as output, via every entry point — `logger(...)`, `.error(...)`,
54
+ `.warn(...)`, and `.tap(value, label?)`. **It does not sanitize them.** (`tap`
55
+ returns the value untouched and renders it for display; it never inspects or
56
+ alters it.)
34
57
 
35
58
  If you log **untrusted data** (user input, third-party API responses, etc.) that
36
59
  contains terminal control or escape sequences (ANSI `\x1b[…`, OSC-8 hyperlinks,
@@ -39,6 +62,9 @@ e.g. overwrite previously printed text or spoof a clickable link. This is an
39
62
  inherent property of writing untrusted text to a terminal, not specific to this
40
63
  package.
41
64
 
65
+ This is worth keeping in mind for `tap`, whose headline use is logging fetched
66
+ API responses — exactly the kind of third-party data to treat as untrusted.
67
+
42
68
  Guidance:
43
69
 
44
70
  - Treat log output as you would any other untrusted text rendered in a terminal.
@@ -32,4 +32,3 @@ export default function getCaller() {
32
32
  return UNKNOWN;
33
33
  return { label: `${base}:${line}`, file, line };
34
34
  }
35
- //# sourceMappingURL=getCaller.js.map
@@ -14,4 +14,3 @@ export interface Caller {
14
14
  * (e.g. minified, eval, or native code).
15
15
  */
16
16
  export default function getCaller(): Caller;
17
- //# sourceMappingURL=getCaller.d.ts.map
@@ -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,29 @@
1
+ "use strict";
2
+ /*
3
+ * color.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.colorize = colorize;
10
+ /** ANSI SGR codes for the colors/styles the logger uses. */
11
+ const CODES = {
12
+ red: "38;2;239;68;68", // truecolor #ef4444
13
+ yellow: 33,
14
+ green: 32,
15
+ cyan: "38;5;37", // 256-color deep cyan (#00afaf)
16
+ gray: 90, // bright black — renders as light gray
17
+ teal: "38;5;30", // 256-color teal (#008787)
18
+ dimTeal: "38;5;23", // 256-color darker teal (#005f5f)
19
+ bold: 1,
20
+ };
21
+ const RESET = "\x1b[0m";
22
+ /**
23
+ * Wraps `text` in the ANSI escape codes for `color`, resetting afterwards.
24
+ * Pure string work — deciding *whether* to colorize is the caller's job (see
25
+ * `isTTY`), kept separate so it can be checked once per entry.
26
+ */
27
+ function colorize(text, color) {
28
+ return `\x1b[${CODES[color]}m${text}${RESET}`;
29
+ }
@@ -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,27 @@
1
+ "use strict";
2
+ /*
3
+ * constants.ts
4
+ * Created by Dewan Mobashirul
5
+ * Copyright (c) 2026 dewan-meadown
6
+ * All rights reserved
7
+ *
8
+ * Single home for the logger's layout/behavior constants, so the values that
9
+ * shape an entry live in one place instead of as magic literals across modules.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.DEFAULT_MAX_LINES = exports.MESSAGE_INDENT = exports.SEPARATOR = exports.BRANCH_END = exports.BRANCH = exports.TAG_COLOR = void 0;
13
+ /** The tag color per channel: info/tap → cyan, warn → yellow, error → red. */
14
+ exports.TAG_COLOR = {
15
+ log: "cyan",
16
+ warn: "yellow",
17
+ error: "red",
18
+ };
19
+ /** Glyphs that draw each entry's little tree. */
20
+ exports.BRANCH = "├──"; // the message branch
21
+ exports.BRANCH_END = "└──"; // the last (metadata) branch
22
+ exports.SEPARATOR = "-"; // between the timestamp and the location
23
+ /** Hang-indent for message continuation lines, so they align under the message
24
+ * text (the `├── ` branch is 4 columns wide). */
25
+ exports.MESSAGE_INDENT = "|\t";
26
+ /** Default for the collapse setting: 0 = show every line. */
27
+ exports.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,29 @@
1
+ "use strict";
2
+ /*
3
+ * createLog.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 = createLog;
13
+ const getCaller_js_1 = __importDefault(require("../caller/getCaller.js"));
14
+ const writeLog_js_1 = require("./writeLog.js");
15
+ const config_js_1 = require("../config.js");
16
+ /**
17
+ * Builds a log function bound to a console channel and tag. The returned closure
18
+ * is what the caller invokes directly, so {@link getCaller} resolves the caller's
19
+ * own frame; the resolved caller is then handed to {@link writeLog}, which never
20
+ * touches the stack. Logs only outside production — see {@link isLogAllowed}.
21
+ */
22
+ function createLog(channel, tag) {
23
+ return (...args) => {
24
+ if (!(0, config_js_1.isLogAllowed)())
25
+ return;
26
+ const caller = (0, getCaller_js_1.default)();
27
+ (0, writeLog_js_1.writeLog)({ channel, tag, args, caller });
28
+ };
29
+ }
@@ -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,95 @@
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));
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 location = formatLocation(caller, useColor);
80
+ // Colors (terminal only): tag by level, timestamp teal, location dim teal,
81
+ // branch and separator gray.
82
+ const tagOut = useColor ? (0, color_js_1.colorize)(tag, constants_js_1.TAG_COLOR[channel]) : tag;
83
+ const timeStamp = useColor ? (0, color_js_1.colorize)((0, getTimeStamp_js_1.default)(), "teal") : (0, getTimeStamp_js_1.default)();
84
+ const locOut = useColor
85
+ ? (0, color_js_1.colorize)(`(${location})`, "dimTeal")
86
+ : `(${location})`;
87
+ const connector = useColor ? (0, color_js_1.colorize)(constants_js_1.BRANCH, "gray") : constants_js_1.BRANCH;
88
+ const connectorBottom = useColor ? (0, color_js_1.colorize)(constants_js_1.BRANCH_END, "gray") : constants_js_1.BRANCH_END;
89
+ const separator = useColor ? (0, color_js_1.colorize)(constants_js_1.SEPARATOR, "gray") : constants_js_1.SEPARATOR;
90
+ // Layout: the tag, the message hanging off a `├──` branch, then the timestamp
91
+ // and location on a `└──` branch below. Leading `\n` spaces entries apart.
92
+ const message = renderMessage(args, useColor);
93
+ const meta = `\n${connectorBottom} ${timeStamp} ${separator} ${locOut}`;
94
+ console[channel](`\n${tagOut}`, `\n${connector}`, message, meta);
95
+ }
@@ -12,10 +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;
@@ -8,7 +8,6 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.fileUrl = fileUrl;
10
10
  exports.hyperlink = hyperlink;
11
- exports.supportsHyperlinks = supportsHyperlinks;
12
11
  const node_url_1 = require("node:url");
13
12
  /**
14
13
  * Builds a valid `file://` URL for a path so terminals can open it on click.
@@ -30,15 +29,3 @@ function hyperlink(text, url) {
30
29
  const BEL = "\x07";
31
30
  return `${OSC}${url}${BEL}${text}${OSC}${BEL}`;
32
31
  }
33
- /**
34
- * Whether to emit OSC-8 hyperlinks for the given stream. Driven solely by the
35
- * stream being an interactive terminal (`isTTY`) — no env vars, no config. When
36
- * output is piped or redirected, links are skipped so escapes never end up in
37
- * files or logs. Terminals that don't understand OSC-8 just show the plain text.
38
- */
39
- function supportsHyperlinks(streamName) {
40
- if (typeof process === "undefined")
41
- return false;
42
- const stream = streamName === "stdout" ? process.stdout : process.stderr;
43
- return Boolean(stream?.isTTY);
44
- }
@@ -1,8 +1,16 @@
1
- /** The logger: a callable for info logs, plus `.error` and `.warn` variants. */
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.
package/dist/cjs/index.js CHANGED
@@ -5,9 +5,14 @@
5
5
  * Copyright (c) 2026 dewan-meadown
6
6
  * All rights reserved
7
7
  */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
12
  exports.logger = void 0;
10
- const index_js_1 = require("./utils/index.js");
13
+ const createLog_js_1 = __importDefault(require("./core/createLog.js"));
14
+ const createTap_js_1 = __importDefault(require("./tap/createTap.js"));
15
+ const writeLog_js_1 = require("./core/writeLog.js");
11
16
  /**
12
17
  * Logs to the console, but only outside production. Each line is prefixed with
13
18
  * a level tag, a short local timestamp, and a clickable link to the file it was
@@ -18,16 +23,17 @@ const index_js_1 = require("./utils/index.js");
18
23
  * @example
19
24
  * logger.maxLines = 5 // long messages collapse to 5 lines; 0 = show all
20
25
  */
21
- const logger = Object.assign((0, index_js_1.createLog)("log", "[INFO]"), {
22
- error: (0, index_js_1.createLog)("error", "[ERROR]"),
23
- warn: (0, index_js_1.createLog)("warn", "[WARN]"),
26
+ const logger = Object.assign((0, createLog_js_1.default)("log", "[INFO]"), {
27
+ error: (0, createLog_js_1.default)("error", "[ERROR]"),
28
+ warn: (0, createLog_js_1.default)("warn", "[WARN]"),
29
+ tap: (0, createTap_js_1.default)(),
24
30
  });
25
31
  exports.logger = logger;
26
32
  // `maxLines` is a live getter/setter backed by the shared collapse setting, so
27
33
  // setting it once affects info, error, and warn alike.
28
34
  Object.defineProperty(logger, "maxLines", {
29
- get: index_js_1.getVisibleLines,
30
- set: index_js_1.setVisibleLines,
35
+ get: writeLog_js_1.getVisibleLines,
36
+ set: writeLog_js_1.setVisibleLines,
31
37
  enumerable: true,
32
38
  configurable: true,
33
39
  });
@@ -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;