@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
package/README.md
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
# @meadown/logger
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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,114 @@ yarn add @meadown/logger
|
|
|
27
43
|
import logger from "@meadown/logger"
|
|
28
44
|
|
|
29
45
|
logger("Hello world")
|
|
30
|
-
logger("Auth", "user logged in")
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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)
|
|
62
86
|
```
|
|
63
87
|
|
|
88
|
+
You can immediately see: was it successful? How long did it take? What came
|
|
89
|
+
back? Without opening DevTools.
|
|
90
|
+
|
|
91
|
+
Works with plain values too — logs it, returns it, nothing changes:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const port = logger.tap(5000, "port") // port is still 5000
|
|
95
|
+
const user = logger.tap(await getUser(), "user") // same as without tap
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Clickable source link
|
|
99
|
+
|
|
100
|
+
That `(server.ts:42)` is a **clickable link** — open it and you land on the exact
|
|
101
|
+
line that wrote the log. Works in VS Code, iTerm2, WezTerm, Kitty, and Windows
|
|
102
|
+
Terminal. Degrades to plain text everywhere else.
|
|
103
|
+
|
|
64
104
|
## Color-coded levels
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
|
106
|
+
`[INFO]` cyan · `[WARN]` yellow · `[ERROR]` red. Timestamp and location tinted teal.
|
|
107
|
+
Auto-disabled when output is piped — no escape codes in your log files.
|
|
70
108
|
|
|
71
|
-
|
|
72
|
-
file or another program, everything prints as plain text — no stray color codes in
|
|
73
|
-
your log files. Nothing to configure.
|
|
109
|
+
## Tree layout output
|
|
74
110
|
|
|
75
|
-
|
|
111
|
+
```text
|
|
112
|
+
[INFO]
|
|
113
|
+
├── Auth user logged in
|
|
114
|
+
└── 05-30 04:00:00 PM - (server.ts:42)
|
|
115
|
+
```
|
|
76
116
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
hunting for where a message came from, and the line number is right there in the
|
|
80
|
-
label.
|
|
117
|
+
Level tag, message, timestamp, and location — all in a clean tree. Easy to scan,
|
|
118
|
+
even in a busy terminal.
|
|
81
119
|
|
|
82
|
-
|
|
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.
|
|
120
|
+
## Collapsible messages
|
|
86
121
|
|
|
87
|
-
|
|
122
|
+
```ts
|
|
123
|
+
logger.maxLines = 5 // show 5 lines, then "... N more lines"
|
|
124
|
+
logger.maxLines = 0 // default — show everything
|
|
125
|
+
```
|
|
88
126
|
|
|
89
|
-
|
|
90
|
-
chatty multi-line message is drowning out your terminal, you can cap how many
|
|
91
|
-
lines each message shows:
|
|
127
|
+
### One thing if you re-export it
|
|
92
128
|
|
|
93
129
|
```ts
|
|
94
|
-
|
|
130
|
+
// GOOD — location stays honest
|
|
131
|
+
export { default as logger } from "@meadown/logger"
|
|
95
132
|
|
|
96
|
-
|
|
97
|
-
logger
|
|
133
|
+
// BAD — every log points at this file, not the real caller
|
|
134
|
+
export const logger = (...args) => log(...args)
|
|
98
135
|
```
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
applies to `logger`, `.error`, and `.warn` alike.
|
|
137
|
+
## NODE_ENV
|
|
102
138
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`NODE_ENV`:
|
|
139
|
+
This logger reads `NODE_ENV` to decide when to log. By default it logs everywhere
|
|
140
|
+
except `production`. This is a deliberate dev-focused default — a production-grade
|
|
141
|
+
logger with transport, persistence, and log levels is a separate concern and a
|
|
142
|
+
future direction.
|
|
108
143
|
|
|
109
144
|
| `NODE_ENV` | Logs? |
|
|
110
145
|
| ---------------------------------------- | ------ |
|
|
111
146
|
| not set, `development`, or anything else | shown |
|
|
112
147
|
| `production` | silent |
|
|
113
148
|
|
|
114
|
-
So leave your logs in the code. Once you ship with `NODE_ENV=production`, they just
|
|
115
|
-
quietly step aside.
|
|
116
|
-
|
|
117
149
|
## Security
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
See [SECURITY.md](https://
|
|
121
|
-
for the security model
|
|
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.
|
|
151
|
+
Zero dependencies, no file or network access, nothing persisted.
|
|
152
|
+
See [SECURITY.md](https://github.com/meadown/meadown-logger/blob/main/SECURITY.md)
|
|
153
|
+
for the full security model.
|
|
124
154
|
|
|
125
155
|
## License
|
|
126
156
|
|
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
|
-
##
|
|
32
|
+
## What the logger does with your values
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
@@ -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
|
-
}
|
package/dist/cjs/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.
|
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
|
|
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,
|
|
22
|
-
error: (0,
|
|
23
|
-
warn: (0,
|
|
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:
|
|
30
|
-
set:
|
|
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;
|