@meadown/logger 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -5,8 +5,9 @@ remember _which file_ a message came from — and worse, forgetting to pull thos
5
5
  out before shipping. So I made this.
6
6
 
7
7
  It's basically `console.log` with the rough edges sanded off: every message gets a
8
- label, a timestamp, and the file and line it came from. And it stays quiet in
9
- production, so you can leave your logs where they are and not worry about them.
8
+ level tag, a timestamp, and the file and line it came from as a **clickable link**
9
+ you can open straight from your terminal. And it stays quiet in production, so you
10
+ can leave your logs where they are and not worry about them.
10
11
 
11
12
  No dependencies. No config. Import it and you're done.
12
13
 
@@ -22,7 +23,7 @@ npm install @meadown/logger
22
23
  import customLog from "@meadown/logger"
23
24
 
24
25
  customLog("Hello world")
25
- customLog("Auth", "user logged in") // first word can be a label, if you want one
26
+ customLog("Auth", "user logged in") // every argument is printed as-is, like console.log
26
27
 
27
28
  customLog.warn("This is deprecated")
28
29
  customLog.error("Something went wrong")
@@ -31,13 +32,24 @@ customLog.error("Something went wrong")
31
32
  You'll see something like:
32
33
 
33
34
  ```text
34
- [INFO] 2026-05-30T10:00:00.000Z (server.ts:42) Auth user logged in
35
- [WARN] 2026-05-30T10:00:00.000Z (server.ts:51) This is deprecated
36
- [ERROR] 2026-05-30T10:00:00.000Z (server.ts:60) Something went wrong
35
+ [INFO] 2026-05-30T10:00:00.000Z (server.ts:42)
36
+ Auth user logged in
37
37
  ```
38
38
 
39
- That `(server.ts:42)` bit is the part I missed most with plain `console.log` —
40
- no more hunting for where a message came from.
39
+ Each line is tagged by level `[INFO]`, `[WARN]`, or `[ERROR]` — followed by the
40
+ timestamp and the source location, with your arguments on the next line.
41
+
42
+ ## Click to open the source
43
+
44
+ That `(server.ts:42)` at the end of every log isn't just text — when you're in a
45
+ terminal, it's a **clickable link** that opens the file the log came from. No more
46
+ hunting for where a message came from, and the line number is right there in the
47
+ label.
48
+
49
+ There's nothing to configure. Links show up automatically when output goes to a
50
+ terminal, and when it's piped to a file or another program they quietly drop to
51
+ plain `(server.ts:42)` text — so your log files never get cluttered with escape
52
+ codes.
41
53
 
42
54
  ## What about production?
43
55
 
@@ -6,9 +6,8 @@ export interface LogFN {
6
6
  }
7
7
  /**
8
8
  * Logs to the console, but only outside production. Each line is prefixed with
9
- * an `[INFO]` tag, an ISO timestamp, and the file and line it was called from;
10
- * all arguments are then printed as-is. `.error` and `.warn` behave the same
11
- * with their own tags and console channels.
9
+ * a level tag, an ISO timestamp, and a clickable link to the file and line it
10
+ * was called from; all arguments are then printed as-is.
12
11
  * @example
13
12
  * customLog("Auth", "user logged in")
14
13
  * // [INFO] 2026-05-30T10:00:00.000Z (server.ts:42) Auth user logged in
package/dist/cjs/index.js CHANGED
@@ -8,32 +8,17 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.customLog = void 0;
10
10
  const index_js_1 = require("./utils/index.js");
11
- const config_js_1 = require("./config.js");
12
- /**
13
- * Builds a log function bound to a console channel and tag. The returned closure
14
- * is what the caller invokes directly, so {@link getFileName} still resolves the
15
- * caller's own frame (no extra stack frame is inserted). `console[channel]` is
16
- * looked up at call time, so reassigning `console.log` (e.g. in tests) is
17
- * respected. Logs only outside production — see {@link isLogAllowed}.
18
- */
19
- function createLog(channel, tag) {
20
- return (...args) => {
21
- if ((0, config_js_1.isLogAllowed)())
22
- console[channel](tag, (0, index_js_1.getTimeStamp)(), `(${(0, index_js_1.getFileName)()})`, ...args);
23
- };
24
- }
25
11
  /**
26
12
  * Logs to the console, but only outside production. Each line is prefixed with
27
- * an `[INFO]` tag, an ISO timestamp, and the file and line it was called from;
28
- * all arguments are then printed as-is. `.error` and `.warn` behave the same
29
- * with their own tags and console channels.
13
+ * a level tag, an ISO timestamp, and a clickable link to the file and line it
14
+ * was called from; all arguments are then printed as-is.
30
15
  * @example
31
16
  * customLog("Auth", "user logged in")
32
17
  * // [INFO] 2026-05-30T10:00:00.000Z (server.ts:42) Auth user logged in
33
18
  */
34
- const customLog = Object.assign(createLog("log", "[INFO]"), {
35
- error: createLog("error", "[ERROR]"),
36
- warn: createLog("warn", "[WARN]"),
19
+ const customLog = Object.assign((0, index_js_1.createLog)("log", "[INFO]"), {
20
+ error: (0, index_js_1.createLog)("error", "[ERROR]"),
21
+ warn: (0, index_js_1.createLog)("warn", "[WARN]"),
37
22
  });
38
23
  exports.customLog = customLog;
39
24
  exports.default = customLog;
@@ -0,0 +1,10 @@
1
+ /** The console channels the logger writes to. */
2
+ export type LogChannel = "log" | "error" | "warn";
3
+ /**
4
+ * Builds a log function bound to a console channel and tag. The returned closure
5
+ * is what the caller invokes directly, so {@link getCaller} still resolves the
6
+ * caller's own frame (no extra stack frame is inserted). `console[channel]` is
7
+ * looked up at call time, so reassigning `console.log` (e.g. in tests) is
8
+ * respected. Logs only outside production — see {@link isLogAllowed}.
9
+ */
10
+ export default function createLog(channel: LogChannel, tag: string): (...args: unknown[]) => void;
@@ -0,0 +1,46 @@
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("./getCaller.js"));
14
+ const getTimeStamp_js_1 = __importDefault(require("./getTimeStamp.js"));
15
+ const link_js_1 = require("./link.js");
16
+ const config_js_1 = require("../config.js");
17
+ /**
18
+ * Renders a caller as a `(file:line)` location. When the terminal supports
19
+ * OSC-8 hyperlinks, the location is a clickable link to the exact source line;
20
+ * otherwise it's plain text. Pure (no stack access), so it can be called from a
21
+ * helper without disturbing {@link getCaller}'s frame depth.
22
+ */
23
+ function formatLocation(caller, streamName) {
24
+ if (caller.file !== null &&
25
+ caller.line !== null &&
26
+ (0, link_js_1.supportsHyperlinks)(streamName))
27
+ return (0, link_js_1.hyperlink)(caller.label, (0, link_js_1.fileUrl)(caller.file));
28
+ return caller.label;
29
+ }
30
+ /**
31
+ * Builds a log function bound to a console channel and tag. The returned closure
32
+ * is what the caller invokes directly, so {@link getCaller} still resolves the
33
+ * caller's own frame (no extra stack frame is inserted). `console[channel]` is
34
+ * looked up at call time, so reassigning `console.log` (e.g. in tests) is
35
+ * respected. Logs only outside production — see {@link isLogAllowed}.
36
+ */
37
+ function createLog(channel, tag) {
38
+ const streamName = channel === "log" ? "stdout" : "stderr";
39
+ return (...args) => {
40
+ if (!(0, config_js_1.isLogAllowed)())
41
+ return;
42
+ const caller = (0, getCaller_js_1.default)();
43
+ const location = formatLocation(caller, streamName);
44
+ console[channel](tag, (0, getTimeStamp_js_1.default)(), `(${location})`, `\n`, ...args, `\n`);
45
+ };
46
+ }
@@ -0,0 +1,16 @@
1
+ /** A resolved call site. `file`/`line` are `null` when they can't be determined. */
2
+ export interface Caller {
3
+ /** Short display location, e.g. `server.ts:42`, or `"unknown"`. */
4
+ label: string;
5
+ /** Absolute path (or `file://` URL) of the calling file, or `null`. */
6
+ file: string | null;
7
+ /** 1-based line number, or `null`. */
8
+ line: number | null;
9
+ }
10
+ /**
11
+ * Reads the call stack and returns the caller's location — both a short display
12
+ * label (`file:line`) and the absolute path/line needed to build a clickable
13
+ * link. Returns {@link UNKNOWN} when the frame isn't a resolvable source file
14
+ * (e.g. minified, eval, or native code).
15
+ */
16
+ export default function getCaller(): Caller;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /*
3
+ * getCaller.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.default = getCaller;
10
+ const UNKNOWN = { label: "unknown", file: null, line: null };
11
+ /** Matches a source file by extension: `.js/.jsx/.ts/.tsx` and `.cjs/.mjs/.cts/.mts`. */
12
+ const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
13
+ /**
14
+ * Reads the call stack and returns the caller's location — both a short display
15
+ * label (`file:line`) and the absolute path/line needed to build a clickable
16
+ * link. Returns {@link UNKNOWN} when the frame isn't a resolvable source file
17
+ * (e.g. minified, eval, or native code).
18
+ */
19
+ function getCaller() {
20
+ const stack = new Error().stack ?? "";
21
+ const frame = stack.split("\n")[3] ?? "";
22
+ // V8 frames look like `at fn (/path/file.js:10:5)` or `at /path/file.js:10:5`
23
+ // (and `file://…` for ESM). Prefer the parenthesised location when present.
24
+ const parens = frame.match(/\(([^)]+)\)\s*$/);
25
+ const inner = (parens?.[1] ?? frame.replace(/^\s*at\s+(?:async\s+)?/, "")).trim();
26
+ // Split off the trailing `:line:column`, keeping the (possibly colon-bearing)
27
+ // path intact (e.g. Windows `C:\…` or `file://…`).
28
+ const match = inner.match(/^(.*):(\d+):\d+$/);
29
+ const file = match?.[1];
30
+ if (file === undefined)
31
+ return UNKNOWN;
32
+ const line = Number(match?.[2]);
33
+ const base = file.split(/[/\\]/).pop() ?? "";
34
+ if (!SOURCE_FILE.test(base))
35
+ return UNKNOWN;
36
+ return { label: `${base}:${line}`, file, line };
37
+ }
@@ -1,2 +1,4 @@
1
- export { default as getFileName } from "./getFileName.js";
1
+ export { default as createLog, type LogChannel } from "./createLog.js";
2
+ export { default as getCaller, type Caller } from "./getCaller.js";
2
3
  export { default as getTimeStamp } from "./getTimeStamp.js";
4
+ export { fileUrl, hyperlink, supportsHyperlinks } from "./link.js";
@@ -9,8 +9,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getTimeStamp = exports.getFileName = void 0;
13
- var getFileName_js_1 = require("./getFileName.js");
14
- Object.defineProperty(exports, "getFileName", { enumerable: true, get: function () { return __importDefault(getFileName_js_1).default; } });
12
+ exports.supportsHyperlinks = exports.hyperlink = exports.fileUrl = exports.getTimeStamp = exports.getCaller = exports.createLog = void 0;
13
+ var createLog_js_1 = require("./createLog.js");
14
+ Object.defineProperty(exports, "createLog", { enumerable: true, get: function () { return __importDefault(createLog_js_1).default; } });
15
+ var getCaller_js_1 = require("./getCaller.js");
16
+ Object.defineProperty(exports, "getCaller", { enumerable: true, get: function () { return __importDefault(getCaller_js_1).default; } });
15
17
  var getTimeStamp_js_1 = require("./getTimeStamp.js");
16
18
  Object.defineProperty(exports, "getTimeStamp", { enumerable: true, get: function () { return __importDefault(getTimeStamp_js_1).default; } });
19
+ var link_js_1 = require("./link.js");
20
+ Object.defineProperty(exports, "fileUrl", { enumerable: true, get: function () { return link_js_1.fileUrl; } });
21
+ Object.defineProperty(exports, "hyperlink", { enumerable: true, get: function () { return link_js_1.hyperlink; } });
22
+ Object.defineProperty(exports, "supportsHyperlinks", { enumerable: true, get: function () { return link_js_1.supportsHyperlinks; } });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Builds a valid `file://` URL for a path so terminals can open it on click.
3
+ * Paths already in `file://` form are used as-is. We intentionally do NOT append
4
+ * `:line` — that isn't a valid URI and breaks file openers (e.g. GNOME/`gio`,
5
+ * which would look for a file literally named `foo.ts:42`). The line number
6
+ * stays visible in the link's display text instead.
7
+ */
8
+ export declare function fileUrl(file: string): string;
9
+ /**
10
+ * Wraps `text` in an OSC-8 terminal hyperlink pointing at `url`. Terminals that
11
+ * support OSC-8 render `text` as a clickable link; others ignore the escape and
12
+ * simply show `text`.
13
+ */
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;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /*
3
+ * link.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.fileUrl = fileUrl;
10
+ exports.hyperlink = hyperlink;
11
+ exports.supportsHyperlinks = supportsHyperlinks;
12
+ const node_url_1 = require("node:url");
13
+ /**
14
+ * Builds a valid `file://` URL for a path so terminals can open it on click.
15
+ * Paths already in `file://` form are used as-is. We intentionally do NOT append
16
+ * `:line` — that isn't a valid URI and breaks file openers (e.g. GNOME/`gio`,
17
+ * which would look for a file literally named `foo.ts:42`). The line number
18
+ * stays visible in the link's display text instead.
19
+ */
20
+ function fileUrl(file) {
21
+ return file.startsWith("file://") ? file : (0, node_url_1.pathToFileURL)(file).href;
22
+ }
23
+ /**
24
+ * Wraps `text` in an OSC-8 terminal hyperlink pointing at `url`. Terminals that
25
+ * support OSC-8 render `text` as a clickable link; others ignore the escape and
26
+ * simply show `text`.
27
+ */
28
+ function hyperlink(text, url) {
29
+ const OSC = "\x1b]8;;";
30
+ const BEL = "\x07";
31
+ return `${OSC}${url}${BEL}${text}${OSC}${BEL}`;
32
+ }
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/index.d.ts CHANGED
@@ -6,9 +6,8 @@ export interface LogFN {
6
6
  }
7
7
  /**
8
8
  * Logs to the console, but only outside production. Each line is prefixed with
9
- * an `[INFO]` tag, an ISO timestamp, and the file and line it was called from;
10
- * all arguments are then printed as-is. `.error` and `.warn` behave the same
11
- * with their own tags and console channels.
9
+ * a level tag, an ISO timestamp, and a clickable link to the file and line it
10
+ * was called from; all arguments are then printed as-is.
12
11
  * @example
13
12
  * customLog("Auth", "user logged in")
14
13
  * // [INFO] 2026-05-30T10:00:00.000Z (server.ts:42) Auth user logged in
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,gFAAgF;AAChF,MAAM,WAAW,KAAK;IACpB,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC1B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC/B,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAC/B;AAmBD;;;;;;;;GAQG;AACH,QAAA,MAAM,SAAS,EAAE,KAGf,CAAA;AAEF,OAAO,EAAE,SAAS,EAAE,CAAA;AACpB,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,gFAAgF;AAChF,MAAM,WAAW,KAAK;IACpB,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC1B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC/B,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;;GAOG;AACH,QAAA,MAAM,SAAS,EAAE,KAGf,CAAA;AAEF,OAAO,EAAE,SAAS,EAAE,CAAA;AACpB,eAAe,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -4,26 +4,11 @@
4
4
  * Copyright (c) 2026 dewan-meadown
5
5
  * All rights reserved
6
6
  */
7
- import { getFileName, getTimeStamp } from "./utils/index.js";
8
- import { isLogAllowed } from "./config.js";
9
- /**
10
- * Builds a log function bound to a console channel and tag. The returned closure
11
- * is what the caller invokes directly, so {@link getFileName} still resolves the
12
- * caller's own frame (no extra stack frame is inserted). `console[channel]` is
13
- * looked up at call time, so reassigning `console.log` (e.g. in tests) is
14
- * respected. Logs only outside production — see {@link isLogAllowed}.
15
- */
16
- function createLog(channel, tag) {
17
- return (...args) => {
18
- if (isLogAllowed())
19
- console[channel](tag, getTimeStamp(), `(${getFileName()})`, ...args);
20
- };
21
- }
7
+ import { createLog } from "./utils/index.js";
22
8
  /**
23
9
  * Logs to the console, but only outside production. Each line is prefixed with
24
- * an `[INFO]` tag, an ISO timestamp, and the file and line it was called from;
25
- * all arguments are then printed as-is. `.error` and `.warn` behave the same
26
- * with their own tags and console channels.
10
+ * a level tag, an ISO timestamp, and a clickable link to the file and line it
11
+ * was called from; all arguments are then printed as-is.
27
12
  * @example
28
13
  * customLog("Auth", "user logged in")
29
14
  * // [INFO] 2026-05-30T10:00:00.000Z (server.ts:42) Auth user logged in
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAS1C;;;;;;GAMG;AACH,SAAS,SAAS,CAChB,OAAiC,EACjC,GAAW;IAEX,OAAO,CAAC,GAAG,IAAe,EAAQ,EAAE;QAClC,IAAI,YAAY,EAAE;YAChB,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,IAAI,WAAW,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACxE,CAAC,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,SAAS,GAAU,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE;IACjE,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC;IACpC,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC;CAClC,CAAC,CAAA;AAEF,OAAO,EAAE,SAAS,EAAE,CAAA;AACpB,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAS5C;;;;;;;GAOG;AACH,MAAM,SAAS,GAAU,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE;IACjE,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC;IACpC,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC;CAClC,CAAC,CAAA;AAEF,OAAO,EAAE,SAAS,EAAE,CAAA;AACpB,eAAe,SAAS,CAAA"}
@@ -0,0 +1,11 @@
1
+ /** The console channels the logger writes to. */
2
+ export type LogChannel = "log" | "error" | "warn";
3
+ /**
4
+ * Builds a log function bound to a console channel and tag. The returned closure
5
+ * is what the caller invokes directly, so {@link getCaller} still resolves the
6
+ * caller's own frame (no extra stack frame is inserted). `console[channel]` is
7
+ * looked up at call time, so reassigning `console.log` (e.g. in tests) is
8
+ * respected. Logs only outside production — see {@link isLogAllowed}.
9
+ */
10
+ export default function createLog(channel: LogChannel, tag: string): (...args: unknown[]) => void;
11
+ //# sourceMappingURL=createLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createLog.d.ts","sourceRoot":"","sources":["../../src/utils/createLog.ts"],"names":[],"mappings":"AAYA,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAA;AAqBjD;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,MAAM,GACV,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAQ9B"}
@@ -0,0 +1,41 @@
1
+ /*
2
+ * createLog.ts
3
+ * Created by Dewan Mobashirul
4
+ * Copyright (c) 2026 dewan-meadown
5
+ * All rights reserved
6
+ */
7
+ import getCaller from "./getCaller.js";
8
+ import getTimeStamp from "./getTimeStamp.js";
9
+ import { fileUrl, hyperlink, supportsHyperlinks } from "./link.js";
10
+ import { isLogAllowed } from "../config.js";
11
+ /**
12
+ * Renders a caller as a `(file:line)` location. When the terminal supports
13
+ * OSC-8 hyperlinks, the location is a clickable link to the exact source line;
14
+ * otherwise it's plain text. Pure (no stack access), so it can be called from a
15
+ * helper without disturbing {@link getCaller}'s frame depth.
16
+ */
17
+ function formatLocation(caller, streamName) {
18
+ if (caller.file !== null &&
19
+ caller.line !== null &&
20
+ supportsHyperlinks(streamName))
21
+ return hyperlink(caller.label, fileUrl(caller.file));
22
+ return caller.label;
23
+ }
24
+ /**
25
+ * Builds a log function bound to a console channel and tag. The returned closure
26
+ * is what the caller invokes directly, so {@link getCaller} still resolves the
27
+ * caller's own frame (no extra stack frame is inserted). `console[channel]` is
28
+ * looked up at call time, so reassigning `console.log` (e.g. in tests) is
29
+ * respected. Logs only outside production — see {@link isLogAllowed}.
30
+ */
31
+ export default function createLog(channel, tag) {
32
+ const streamName = channel === "log" ? "stdout" : "stderr";
33
+ return (...args) => {
34
+ if (!isLogAllowed())
35
+ return;
36
+ const caller = getCaller();
37
+ const location = formatLocation(caller, streamName);
38
+ console[channel](tag, getTimeStamp(), `(${location})`, `\n`, ...args, `\n`);
39
+ };
40
+ }
41
+ //# sourceMappingURL=createLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createLog.js","sourceRoot":"","sources":["../../src/utils/createLog.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,SAA0B,MAAM,gBAAgB,CAAA;AACvD,OAAO,YAAY,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAK3C;;;;;GAKG;AACH,SAAS,cAAc,CACrB,MAAc,EACd,UAA+B;IAE/B,IACE,MAAM,CAAC,IAAI,KAAK,IAAI;QACpB,MAAM,CAAC,IAAI,KAAK,IAAI;QACpB,kBAAkB,CAAC,UAAU,CAAC;QAE9B,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACtD,OAAO,MAAM,CAAC,KAAK,CAAA;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,OAAmB,EACnB,GAAW;IAEX,MAAM,UAAU,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAA;IAC1D,OAAO,CAAC,GAAG,IAAe,EAAQ,EAAE;QAClC,IAAI,CAAC,YAAY,EAAE;YAAE,OAAM;QAC3B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;QAC1B,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;QACnD,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,IAAI,QAAQ,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,CAAC,CAAA;IAC7E,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /** A resolved call site. `file`/`line` are `null` when they can't be determined. */
2
+ export interface Caller {
3
+ /** Short display location, e.g. `server.ts:42`, or `"unknown"`. */
4
+ label: string;
5
+ /** Absolute path (or `file://` URL) of the calling file, or `null`. */
6
+ file: string | null;
7
+ /** 1-based line number, or `null`. */
8
+ line: number | null;
9
+ }
10
+ /**
11
+ * Reads the call stack and returns the caller's location — both a short display
12
+ * label (`file:line`) and the absolute path/line needed to build a clickable
13
+ * link. Returns {@link UNKNOWN} when the frame isn't a resolvable source file
14
+ * (e.g. minified, eval, or native code).
15
+ */
16
+ export default function getCaller(): Caller;
17
+ //# sourceMappingURL=getCaller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getCaller.d.ts","sourceRoot":"","sources":["../../src/utils/getCaller.ts"],"names":[],"mappings":"AAOA,oFAAoF;AACpF,MAAM,WAAW,MAAM;IACrB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,sCAAsC;IACtC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAOD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,IAAI,MAAM,CAsB1C"}
@@ -0,0 +1,35 @@
1
+ /*
2
+ * getCaller.ts
3
+ * Created by Dewan Mobashirul
4
+ * Copyright (c) 2026 dewan-meadown
5
+ * All rights reserved
6
+ */
7
+ const UNKNOWN = { label: "unknown", file: null, line: null };
8
+ /** Matches a source file by extension: `.js/.jsx/.ts/.tsx` and `.cjs/.mjs/.cts/.mts`. */
9
+ const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
10
+ /**
11
+ * Reads the call stack and returns the caller's location — both a short display
12
+ * label (`file:line`) and the absolute path/line needed to build a clickable
13
+ * link. Returns {@link UNKNOWN} when the frame isn't a resolvable source file
14
+ * (e.g. minified, eval, or native code).
15
+ */
16
+ export default function getCaller() {
17
+ const stack = new Error().stack ?? "";
18
+ const frame = stack.split("\n")[3] ?? "";
19
+ // V8 frames look like `at fn (/path/file.js:10:5)` or `at /path/file.js:10:5`
20
+ // (and `file://…` for ESM). Prefer the parenthesised location when present.
21
+ const parens = frame.match(/\(([^)]+)\)\s*$/);
22
+ const inner = (parens?.[1] ?? frame.replace(/^\s*at\s+(?:async\s+)?/, "")).trim();
23
+ // Split off the trailing `:line:column`, keeping the (possibly colon-bearing)
24
+ // path intact (e.g. Windows `C:\…` or `file://…`).
25
+ const match = inner.match(/^(.*):(\d+):\d+$/);
26
+ const file = match?.[1];
27
+ if (file === undefined)
28
+ return UNKNOWN;
29
+ const line = Number(match?.[2]);
30
+ const base = file.split(/[/\\]/).pop() ?? "";
31
+ if (!SOURCE_FILE.test(base))
32
+ return UNKNOWN;
33
+ return { label: `${base}:${line}`, file, line };
34
+ }
35
+ //# sourceMappingURL=getCaller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getCaller.js","sourceRoot":"","sources":["../../src/utils/getCaller.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,MAAM,OAAO,GAAW,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AAEpE,yFAAyF;AACzF,MAAM,WAAW,GAAG,iBAAiB,CAAA;AAErC;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAExC,8EAA8E;IAC9E,4EAA4E;IAC5E,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAA;IAC7C,MAAM,KAAK,GAAG,CACZ,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAC3D,CAAC,IAAI,EAAE,CAAA;IAER,8EAA8E;IAC9E,mDAAmD;IACnD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;IAC7C,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;IACvB,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,OAAO,CAAA;IAEtC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;IAC5C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAA;IAE3C,OAAO,EAAE,KAAK,EAAE,GAAG,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AACjD,CAAC"}
@@ -1,3 +1,5 @@
1
- export { default as getFileName } from "./getFileName.js";
1
+ export { default as createLog, type LogChannel } from "./createLog.js";
2
+ export { default as getCaller, type Caller } from "./getCaller.js";
2
3
  export { default as getTimeStamp } from "./getTimeStamp.js";
4
+ export { fileUrl, hyperlink, supportsHyperlinks } from "./link.js";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACtE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA"}
@@ -4,6 +4,8 @@
4
4
  * Copyright (c) 2026 dewan-meadown
5
5
  * All rights reserved
6
6
  */
7
- export { default as getFileName } from "./getFileName.js";
7
+ export { default as createLog } from "./createLog.js";
8
+ export { default as getCaller } from "./getCaller.js";
8
9
  export { default as getTimeStamp } from "./getTimeStamp.js";
10
+ export { fileUrl, hyperlink, supportsHyperlinks } from "./link.js";
9
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,SAAS,EAAmB,MAAM,gBAAgB,CAAA;AACtE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAe,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Builds a valid `file://` URL for a path so terminals can open it on click.
3
+ * Paths already in `file://` form are used as-is. We intentionally do NOT append
4
+ * `:line` — that isn't a valid URI and breaks file openers (e.g. GNOME/`gio`,
5
+ * which would look for a file literally named `foo.ts:42`). The line number
6
+ * stays visible in the link's display text instead.
7
+ */
8
+ export declare function fileUrl(file: string): string;
9
+ /**
10
+ * Wraps `text` in an OSC-8 terminal hyperlink pointing at `url`. Terminals that
11
+ * support OSC-8 render `text` as a clickable link; others ignore the escape and
12
+ * simply show `text`.
13
+ */
14
+ export declare function hyperlink(text: string, url: string): string;
15
+ /**
16
+ * Whether to emit OSC-8 hyperlinks for the given stream. Driven solely by the
17
+ * stream being an interactive terminal (`isTTY`) — no env vars, no config. When
18
+ * output is piped or redirected, links are skipped so escapes never end up in
19
+ * files or logs. Terminals that don't understand OSC-8 just show the plain text.
20
+ */
21
+ export declare function supportsHyperlinks(streamName: "stdout" | "stderr"): boolean;
22
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/utils/link.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAI3E"}
@@ -0,0 +1,40 @@
1
+ /*
2
+ * link.ts
3
+ * Created by Dewan Mobashirul
4
+ * Copyright (c) 2026 dewan-meadown
5
+ * All rights reserved
6
+ */
7
+ import { pathToFileURL } from "node:url";
8
+ /**
9
+ * Builds a valid `file://` URL for a path so terminals can open it on click.
10
+ * Paths already in `file://` form are used as-is. We intentionally do NOT append
11
+ * `:line` — that isn't a valid URI and breaks file openers (e.g. GNOME/`gio`,
12
+ * which would look for a file literally named `foo.ts:42`). The line number
13
+ * stays visible in the link's display text instead.
14
+ */
15
+ export function fileUrl(file) {
16
+ return file.startsWith("file://") ? file : pathToFileURL(file).href;
17
+ }
18
+ /**
19
+ * Wraps `text` in an OSC-8 terminal hyperlink pointing at `url`. Terminals that
20
+ * support OSC-8 render `text` as a clickable link; others ignore the escape and
21
+ * simply show `text`.
22
+ */
23
+ export function hyperlink(text, url) {
24
+ const OSC = "\x1b]8;;";
25
+ const BEL = "\x07";
26
+ return `${OSC}${url}${BEL}${text}${OSC}${BEL}`;
27
+ }
28
+ /**
29
+ * Whether to emit OSC-8 hyperlinks for the given stream. Driven solely by the
30
+ * stream being an interactive terminal (`isTTY`) — no env vars, no config. When
31
+ * output is piped or redirected, links are skipped so escapes never end up in
32
+ * files or logs. Terminals that don't understand OSC-8 just show the plain text.
33
+ */
34
+ export function supportsHyperlinks(streamName) {
35
+ if (typeof process === "undefined")
36
+ return false;
37
+ const stream = streamName === "stdout" ? process.stdout : process.stderr;
38
+ return Boolean(stream?.isTTY);
39
+ }
40
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../../src/utils/link.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAA;AACrE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,GAAW;IACjD,MAAM,GAAG,GAAG,UAAU,CAAA;IACtB,MAAM,GAAG,GAAG,MAAM,CAAA;IAClB,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,GAAG,EAAE,CAAA;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAA+B;IAChE,IAAI,OAAO,OAAO,KAAK,WAAW;QAAE,OAAO,KAAK,CAAA;IAChD,MAAM,MAAM,GAAG,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAA;IACxE,OAAO,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AAC/B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meadown/logger",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A tiny, zero-dependency logger for Node.js and TypeScript that tags each line, timestamps it, shows the source file and line, and goes quiet in production.",
5
5
  "keywords": [
6
6
  "logger",
@@ -1,5 +0,0 @@
1
- /**
2
- * Returns the caller's source location as `file.ext:line` by reading the call
3
- * stack, or `"unknown"` if it can't be determined (e.g. minified or eval code).
4
- */
5
- export default function getFileName(): string;
@@ -1,19 +0,0 @@
1
- "use strict";
2
- /*
3
- * getFileName.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.default = getFileName;
10
- /**
11
- * Returns the caller's source location as `file.ext:line` by reading the call
12
- * stack, or `"unknown"` if it can't be determined (e.g. minified or eval code).
13
- */
14
- function getFileName() {
15
- const stack = new Error().stack ?? "";
16
- const line = stack.split("\n")[3] ?? "";
17
- const match = line.match(/([^/\\]+\.[cm]?[jt]sx?):(\d+)/);
18
- return match ? match[0] : "unknown";
19
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Returns the caller's source location as `file.ext:line` by reading the call
3
- * stack, or `"unknown"` if it can't be determined (e.g. minified or eval code).
4
- */
5
- export default function getFileName(): string;
6
- //# sourceMappingURL=getFileName.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"getFileName.d.ts","sourceRoot":"","sources":["../../src/utils/getFileName.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,IAAI,MAAM,CAK5C"}
@@ -1,17 +0,0 @@
1
- /*
2
- * getFileName.ts
3
- * Created by Dewan Mobashirul
4
- * Copyright (c) 2026 dewan-meadown
5
- * All rights reserved
6
- */
7
- /**
8
- * Returns the caller's source location as `file.ext:line` by reading the call
9
- * stack, or `"unknown"` if it can't be determined (e.g. minified or eval code).
10
- */
11
- export default function getFileName() {
12
- const stack = new Error().stack ?? "";
13
- const line = stack.split("\n")[3] ?? "";
14
- const match = line.match(/([^/\\]+\.[cm]?[jt]sx?):(\d+)/);
15
- return match ? match[0] : "unknown";
16
- }
17
- //# sourceMappingURL=getFileName.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"getFileName.js","sourceRoot":"","sources":["../../src/utils/getFileName.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW;IACjC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;IACrC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAA;IACzD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACrC,CAAC"}