logxpert 2.0.3 → 2.1.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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Colorful console logging for Node.js with optional daily file rotation.
4
4
 
5
+ - Zero heavy dependencies — built-in engine.
6
+ - Daily file rotation with retention, size limits and gzip.
7
+ - Per-level colors, lazy messages, child loggers, isolated instances.
8
+ - Works from JS and TS, CommonJS and ESM.
9
+
5
10
  ## Installation
6
11
 
7
12
  ```sh
@@ -64,9 +69,13 @@ log.settings({
64
69
  enableTimestamp: true,
65
70
  timestampFormat: 'YYYY-MM-DD HH:mm:ss',
66
71
  colorize: true,
72
+ colors: { info: 'blue', error: false }, // names, ANSI 0-255, or false per level
67
73
  },
68
74
  });
69
75
 
76
+ // Default colors: error red, warn yellow, info green, http magenta,
77
+ // verbose cyan, debug blue, silly gray. Only the [level] tag is colored.
78
+
70
79
  const { createLogger } = require('logxpert');
71
80
  const apiLog = createLogger({ level: 'info' }); // independent instance
72
81
 
@@ -84,6 +93,8 @@ log.settings({
84
93
  timestampPrefix: '',
85
94
  timestampSuffix: '',
86
95
  colorize: true,
96
+ colors: { info: 'blue', warn: 208, error: false }
97
+ // enabled: false, // file-only logging: silence the console
87
98
  },
88
99
  files: {
89
100
  folder: 'logs',
package/index.d.mts CHANGED
@@ -14,6 +14,7 @@ export const info: typeof log.info;
14
14
  export const debug: typeof log.debug;
15
15
 
16
16
  export type LogLevel = log.LogLevel;
17
+ export type LevelColor = log.LevelColor;
17
18
  export type ConsoleOptions = log.ConsoleOptions;
18
19
  export type FilesOptions = log.FilesOptions;
19
20
  export type RunNumberOptions = log.RunNumberOptions;
package/index.d.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  declare namespace log {
2
2
  export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
3
3
 
4
+ export type LevelColor = string | number | false | null;
5
+
4
6
  export interface ConsoleOptions {
7
+ enabled?: boolean;
5
8
  enableTimestamp?: boolean;
6
9
  timestampFormat?: string;
7
10
  timestampPrefix?: string;
8
11
  timestampSuffix?: string;
9
12
  colorize?: boolean;
13
+ colors?: Partial<Record<LogLevel, LevelColor>>;
10
14
  level?: LogLevel;
11
15
  }
12
16
 
@@ -42,7 +46,7 @@ declare namespace log {
42
46
  }
43
47
 
44
48
  export interface LogSettingsOptions {
45
- console?: ConsoleOptions;
49
+ console?: ConsoleOptions | false;
46
50
  files?: FilesOptions;
47
51
  level?: LogLevel;
48
52
  }
package/lib/files.js ADDED
@@ -0,0 +1,102 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { sanitizeAppName, isPadChar, assertFilenameSafe } = require('./sanitize');
4
+
5
+ let cachedAppName = null;
6
+ function getDefaultAppName() {
7
+ if (cachedAppName) return cachedAppName;
8
+ try {
9
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
10
+ const raw = fs.readFileSync(pkgPath, 'utf8');
11
+ cachedAppName = sanitizeAppName(JSON.parse(raw)?.name) ?? 'application';
12
+ } catch {
13
+ cachedAppName = 'application';
14
+ }
15
+ return cachedAppName;
16
+ }
17
+
18
+ function resolveRunNumber(folder, prefix, opt) {
19
+ let enabled = false;
20
+ let separator = '-';
21
+ let padding = 0;
22
+ let startAt = 1;
23
+ if (opt === true) {
24
+ enabled = true;
25
+ } else if (opt && typeof opt === 'object') {
26
+ enabled = opt.enabled ?? true;
27
+ if (opt.separator !== undefined) {
28
+ if (typeof opt.separator !== 'string' || !opt.separator) throw new Error('files.runNumber.separator must be a non-empty string');
29
+ separator = opt.separator;
30
+ }
31
+ if (opt.padding !== undefined) {
32
+ if (!Number.isInteger(opt.padding) || opt.padding < 0) throw new Error('files.runNumber.padding must be a non-negative integer');
33
+ padding = opt.padding;
34
+ }
35
+ if (opt.startAt !== undefined) {
36
+ if (!Number.isInteger(opt.startAt) || opt.startAt < 0) throw new Error('files.runNumber.startAt must be a non-negative integer');
37
+ startAt = opt.startAt;
38
+ }
39
+ } else if (opt !== undefined && opt !== false) {
40
+ throw new Error('files.runNumber must be a boolean or an object');
41
+ }
42
+ if (!enabled) return null;
43
+ const counterFile = path.join(folder, `.${prefix}.run`);
44
+ let next = startAt;
45
+ try {
46
+ const raw = fs.readFileSync(counterFile, 'utf8').trim();
47
+ const prev = parseInt(raw, 10);
48
+ next = Number.isInteger(prev) ? prev + 1 : startAt;
49
+ } catch {}
50
+ try {
51
+ fs.writeFileSync(counterFile, String(next), 'utf8');
52
+ } catch {}
53
+ return separator + String(next).padStart(padding, '0');
54
+ }
55
+
56
+ function normalizeFilesOptions(files = {}) {
57
+ const rawFolder = files.folder ?? 'logs';
58
+ if (typeof rawFolder !== 'string' || !rawFolder.trim() || rawFolder.includes('\0')) {
59
+ throw new Error('files.folder must be a non-empty string');
60
+ }
61
+ const folder = path.normalize(rawFolder);
62
+ const datePattern = files.datePattern ?? files.dateFormat ?? files.filesName ?? 'YYYY_MM_DD';
63
+ if (typeof datePattern !== 'string' || !datePattern.trim()) {
64
+ throw new Error('files.datePattern must be a non-empty string');
65
+ }
66
+ const explicitPattern = files.filePattern ?? files.pattern ?? files.filename;
67
+ const explicitPrefix = files.prefix ?? files.appName;
68
+ if (explicitPattern !== undefined && explicitPrefix !== undefined) {
69
+ throw new Error('Do not combine "prefix" with "filePattern", pick one: prefix builds "<prefix>-%DATE%.log" for you, filePattern takes full control');
70
+ }
71
+ let filename = explicitPattern ?? `${sanitizeAppName(explicitPrefix) ?? getDefaultAppName()}-%DATE%.log`;
72
+ filename = filename.split('%datePattern%').join('%DATE%');
73
+ assertFilenameSafe(filename);
74
+ if (!filename.includes('%DATE%')) {
75
+ const ext = path.extname(filename) || '.log';
76
+ const base = path.basename(filename, ext);
77
+ filename = `${base}-%DATE%${ext}`;
78
+ }
79
+ assertFilenameSafe(filename);
80
+ if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
81
+ const rawPrefix = path.basename(filename).split('%DATE%')[0];
82
+ let prefixEnd = rawPrefix.length;
83
+ while (prefixEnd > 0 && isPadChar(rawPrefix[prefixEnd - 1])) prefixEnd--;
84
+ const prefix = rawPrefix.slice(0, prefixEnd) || 'application';
85
+ const runSuffix = resolveRunNumber(folder, sanitizeAppName(prefix) ?? 'application', files.runNumber ?? false);
86
+ if (runSuffix) {
87
+ const ext = path.extname(filename) || '.log';
88
+ const base = filename.slice(0, -ext.length);
89
+ filename = `${base}${runSuffix}${ext}`;
90
+ }
91
+ return {
92
+ folder,
93
+ datePattern,
94
+ filename,
95
+ maxFiles: files.maxFile ?? files.maxFiles ?? '14d',
96
+ maxSize: files.maxSize ?? '20m',
97
+ zippedArchive: files.zippedArchive ?? false,
98
+ level: files.level,
99
+ };
100
+ }
101
+
102
+ module.exports = { getDefaultAppName, resolveRunNumber, normalizeFilesOptions };
package/lib/format.js ADDED
@@ -0,0 +1,38 @@
1
+ const util = require('util');
2
+
3
+ function formatExtra(meta) {
4
+ const keys = Object.keys(meta).filter((k) => k !== 'splat' && k !== 'ms');
5
+ if (!keys.length) return '';
6
+ try {
7
+ const picked = {};
8
+ for (const k of keys) picked[k] = meta[k];
9
+ return JSON.stringify(picked);
10
+ } catch {
11
+ return '';
12
+ }
13
+ }
14
+
15
+ function mergeMeta(meta) {
16
+ const out = {};
17
+ for (const m of meta) {
18
+ if (m && typeof m === 'object') Object.assign(out, m);
19
+ }
20
+ return out;
21
+ }
22
+
23
+ function renderBody(message, meta) {
24
+ if (typeof message === 'string' && meta.length && /%[sdifoO%]/.test(message)) {
25
+ return util.format(message, ...meta);
26
+ }
27
+ const msg = typeof message === 'string' ? message : message == null ? '' : JSON.stringify(message);
28
+ const extra = formatExtra(mergeMeta(meta));
29
+ return [msg, extra].filter(Boolean).join(' ');
30
+ }
31
+
32
+ function buildLine(timestamp, level, message, meta, tag) {
33
+ const body = renderBody(message, meta);
34
+ const label = tag ?? `[${level}]`;
35
+ return timestamp ? `${timestamp} ${label}: ${body}` : `${label}: ${body}`;
36
+ }
37
+
38
+ module.exports = { formatExtra, mergeMeta, renderBody, buildLine };
package/lib/mini.js ADDED
@@ -0,0 +1,134 @@
1
+ const LEVELS = {
2
+ error: 0,
3
+ warn: 1,
4
+ info: 2,
5
+ http: 3,
6
+ verbose: 4,
7
+ debug: 5,
8
+ silly: 6,
9
+ };
10
+
11
+ const COLORS = {
12
+ error: 31,
13
+ warn: 33,
14
+ info: 32,
15
+ http: 35,
16
+ verbose: 36,
17
+ debug: 34,
18
+ silly: 90,
19
+ };
20
+
21
+ const NAMED_COLORS = {
22
+ black: 30,
23
+ red: 31,
24
+ green: 32,
25
+ yellow: 33,
26
+ blue: 34,
27
+ magenta: 35,
28
+ cyan: 36,
29
+ white: 37,
30
+ gray: 90,
31
+ grey: 90,
32
+ };
33
+
34
+ function resolveColorCode(value, level) {
35
+ if (value === undefined || value === null || value === false) return null;
36
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 255) return value;
37
+ if (typeof value === 'string' && NAMED_COLORS[value.toLowerCase()] !== undefined) return NAMED_COLORS[value.toLowerCase()];
38
+ throw new Error(`Unknown color "${value}" for level "${level}". Use one of: ${Object.keys(NAMED_COLORS).join(', ')}, an ANSI code 0-255, or false to disable`);
39
+ }
40
+
41
+ function resolveLevelColors(custom = {}) {
42
+ if (custom === null || typeof custom !== 'object' || Array.isArray(custom)) {
43
+ throw new Error('console.colors must be an object mapping level names to colors');
44
+ }
45
+ const map = {};
46
+ for (const level of Object.keys(COLORS)) {
47
+ if (custom[level] !== undefined) {
48
+ const code = resolveColorCode(custom[level], level);
49
+ if (code !== null) map[level] = code;
50
+ } else {
51
+ map[level] = COLORS[level];
52
+ }
53
+ }
54
+ return map;
55
+ }
56
+
57
+ function colorizeLevel(level, text, colors = COLORS) {
58
+ const code = colors[level] ?? 0;
59
+ if (!code) return text;
60
+ return `\x1b[${code}m${text}\x1b[0m`;
61
+ }
62
+
63
+ class NullTransport {
64
+ constructor() {
65
+ this.level = 'off';
66
+ }
67
+
68
+ write() {}
69
+
70
+ close() {}
71
+ }
72
+
73
+ class ConsoleTransport {
74
+ constructor({ level, render } = {}) {
75
+ this.level = level;
76
+ this.render = render;
77
+ }
78
+
79
+ write(entry) {
80
+ const line = this.render(entry);
81
+ const stream = entry.level === 'error' || entry.level === 'warn' ? process.stderr : process.stdout;
82
+ stream.write(line + '\n');
83
+ }
84
+
85
+ close() {}
86
+ }
87
+
88
+ class MiniLogger {
89
+ constructor({ level = 'debug' } = {}) {
90
+ this.level = level;
91
+ this.levels = LEVELS;
92
+ this.transports = [];
93
+ }
94
+
95
+ add(transport) {
96
+ this.transports.push(transport);
97
+ return this;
98
+ }
99
+
100
+ remove(transport) {
101
+ const i = this.transports.indexOf(transport);
102
+ if (i >= 0) this.transports.splice(i, 1);
103
+ try {
104
+ transport.close?.();
105
+ } catch {}
106
+ return this;
107
+ }
108
+
109
+ enabled(level, transportLevel) {
110
+ const want = LEVELS[level];
111
+ const current = LEVELS[transportLevel ?? this.level];
112
+ return want !== undefined && current !== undefined && want <= current;
113
+ }
114
+
115
+ log(level, message, ...meta) {
116
+ for (const t of [...this.transports]) {
117
+ if (this.enabled(level, t.level)) t.write({ level, message, meta });
118
+ }
119
+ }
120
+
121
+ child(meta = {}) {
122
+ const self = this;
123
+ const make = (level) => (message, ...args) => self.log(level, message, meta, ...args);
124
+ return {
125
+ error: make('error'),
126
+ warn: make('warn'),
127
+ info: make('info'),
128
+ debug: make('debug'),
129
+ log: (level, message, ...args) => self.log(level, message, meta, ...args),
130
+ };
131
+ }
132
+ }
133
+
134
+ module.exports = { LEVELS, ConsoleTransport, NullTransport, MiniLogger, colorizeLevel, resolveLevelColors, NAMED_COLORS };
package/lib/rotate.js ADDED
@@ -0,0 +1,144 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const zlib = require('zlib');
4
+ const dayjs = require('dayjs');
5
+
6
+ function parseSize(value, fallback) {
7
+ if (value == null) return fallback;
8
+ const m = String(value).trim().match(/^(\d+(?:\.\d+)?)\s*([kmg])?$/i);
9
+ if (!m) return fallback;
10
+ const mult = { k: 1024, m: 1024 ** 2, g: 1024 ** 3 }[(m[2] || '').toLowerCase()] ?? 1;
11
+ return Math.floor(parseFloat(m[1]) * mult);
12
+ }
13
+
14
+ function parseDays(value) {
15
+ if (value == null) return null;
16
+ const m = String(value).trim().match(/^(\d+)\s*d$/i);
17
+ return m ? parseInt(m[1], 10) : null;
18
+ }
19
+
20
+ class FileTransport {
21
+ constructor({ folder, filename, datePattern, maxSize = '20m', maxFiles = '14d', zippedArchive = false, level, render }) {
22
+ this.dirname = folder;
23
+ this.filename = filename;
24
+ this.options = { datePattern, maxSize, maxFiles, zippedArchive };
25
+ this.level = level;
26
+ this.render = render;
27
+ this.datePattern = datePattern;
28
+ this.maxBytes = parseSize(maxSize, Infinity);
29
+ this.maxFiles = maxFiles;
30
+ this.maxDays = parseDays(maxFiles);
31
+ this.zippedArchive = zippedArchive;
32
+ this.currentKey = null;
33
+ this.currentFile = null;
34
+ this.stream = null;
35
+ this.streamSize = 0;
36
+ this.streamErr = null;
37
+ }
38
+
39
+ fileNameFor(dateStr, n = 0) {
40
+ let name = this.filename.split('%DATE%').join(dateStr);
41
+ if (n > 0) {
42
+ const ext = path.extname(name) || '.log';
43
+ name = name.slice(0, -ext.length) + '-' + n + ext;
44
+ }
45
+ return name;
46
+ }
47
+
48
+ open(dateStr, startN = 0) {
49
+ if (!fs.existsSync(this.dirname)) fs.mkdirSync(this.dirname, { recursive: true });
50
+ let n = startN;
51
+ let full = path.join(this.dirname, this.fileNameFor(dateStr, n));
52
+ try {
53
+ let size = fs.existsSync(full) ? fs.statSync(full).size : 0;
54
+ while (size >= this.maxBytes && n < 1000) {
55
+ n += 1;
56
+ full = path.join(this.dirname, this.fileNameFor(dateStr, n));
57
+ size = fs.existsSync(full) ? fs.statSync(full).size : 0;
58
+ }
59
+ } catch {}
60
+ const stream = fs.createWriteStream(full, { flags: 'a' });
61
+ stream.on('error', (err) => { this.streamErr = err; });
62
+ this.stream = stream;
63
+ this.streamSize = 0;
64
+ try {
65
+ this.streamSize = fs.existsSync(full) ? fs.statSync(full).size : 0;
66
+ } catch {}
67
+ this.currentKey = dateStr + '#' + n;
68
+ this.currentFile = full;
69
+ }
70
+
71
+ rotateIfNeeded(dateStr, incoming) {
72
+ if (this.currentKey !== null && !this.currentKey.startsWith(dateStr + '#')) {
73
+ const previous = this.currentFile;
74
+ this.closeStream();
75
+ this.onDateChanged(previous);
76
+ this.currentKey = null;
77
+ }
78
+ if (this.stream === null) {
79
+ this.open(dateStr);
80
+ } else if (this.streamSize + incoming > this.maxBytes) {
81
+ const cur = parseInt(String(this.currentKey || '#0').split('#')[1] || '0', 10);
82
+ this.closeStream();
83
+ this.open(dateStr, cur + 1);
84
+ }
85
+ }
86
+
87
+ onDateChanged(previousFile) {
88
+ if (!previousFile) return;
89
+ try {
90
+ if (this.zippedArchive && fs.existsSync(previousFile)) {
91
+ const gz = previousFile + '.gz';
92
+ const data = fs.readFileSync(previousFile);
93
+ fs.writeFileSync(gz, zlib.gzipSync(data));
94
+ fs.rmSync(previousFile, { force: true });
95
+ }
96
+ } catch {}
97
+ this.cleanupOld();
98
+ }
99
+
100
+ cleanupOld() {
101
+ if (this.maxDays == null) return;
102
+ let entries;
103
+ try {
104
+ entries = fs.readdirSync(this.dirname);
105
+ } catch {
106
+ return;
107
+ }
108
+ const cutoff = Date.now() - this.maxDays * 86400000;
109
+ for (const entry of entries) {
110
+ if (!entry.endsWith('.log') && !entry.endsWith('.log.gz') && !entry.endsWith('.gz')) continue;
111
+ const full = path.join(this.dirname, entry);
112
+ try {
113
+ if (fs.statSync(full).mtimeMs < cutoff) fs.rmSync(full, { force: true });
114
+ } catch {}
115
+ }
116
+ }
117
+
118
+ write(entry) {
119
+ const line = this.render(entry);
120
+ const buf = Buffer.byteLength(line + '\n');
121
+ const dateStr = dayjs().format(this.datePattern);
122
+ this.rotateIfNeeded(dateStr, buf);
123
+ if (this.stream) {
124
+ this.stream.write(line + '\n');
125
+ this.streamSize += buf;
126
+ }
127
+ }
128
+
129
+ closeStream() {
130
+ if (this.stream) {
131
+ try {
132
+ this.stream.end();
133
+ } catch {}
134
+ this.stream = null;
135
+ }
136
+ }
137
+
138
+ close() {
139
+ this.closeStream();
140
+ this.currentKey = null;
141
+ }
142
+ }
143
+
144
+ module.exports = { FileTransport, parseSize, parseDays };
@@ -0,0 +1,89 @@
1
+ const path = require('path');
2
+
3
+ const VALID_LEVELS = ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'];
4
+
5
+ function assertLevel(level) {
6
+ if (!VALID_LEVELS.includes(level)) throw new Error(`Invalid log level "${level}". Use one of: ${VALID_LEVELS.join(', ')}`);
7
+ }
8
+
9
+ function stripAnsi(str) {
10
+ let out = '';
11
+ let i = 0;
12
+ while (i < str.length) {
13
+ if (str[i] !== '\x1b') {
14
+ out += str[i];
15
+ i++;
16
+ continue;
17
+ }
18
+ const next = str[i + 1];
19
+ if (next === ']') {
20
+ let j = i + 2;
21
+ while (j < str.length) {
22
+ if (str[j] === '\x07') { j++; break; }
23
+ if (str[j] === '\x1b' && str[j + 1] === '\\') { j += 2; break; }
24
+ j++;
25
+ }
26
+ i = j;
27
+ } else if (next === '[') {
28
+ let j = i + 2;
29
+ while (j < str.length && str[j] >= ' ' && str[j] <= '?') j++;
30
+ if (j < str.length) j++;
31
+ i = j;
32
+ } else if (next === '(' || next === ')') {
33
+ i += 3;
34
+ } else {
35
+ i += next === undefined ? 1 : 2;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ const CONTROL_RE = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/g;
41
+
42
+ function sanitizeMessage(msg) {
43
+ if (typeof msg !== 'string') return msg;
44
+ return stripAnsi(msg).replace(CONTROL_RE, '');
45
+ }
46
+
47
+ function assertFilenameSafe(filename) {
48
+ if (typeof filename !== 'string' || !filename.trim() || filename.includes('\0')) {
49
+ throw new Error('files.filename must be a non-empty string');
50
+ }
51
+ if (path.isAbsolute(filename) || filename.split(/[\\/]/).includes('..')) {
52
+ throw new Error('files.filename must not escape the log folder (.. and absolute paths are rejected)');
53
+ }
54
+ }
55
+
56
+ function isSafeChar(c) {
57
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '.' || c === '_' || c === '-';
58
+ }
59
+
60
+ function isPadChar(c) {
61
+ return c === '-' || c === '_' || c === '.';
62
+ }
63
+
64
+ function sanitizeAppName(name) {
65
+ if (typeof name !== 'string' || !name.trim()) return null;
66
+ const base = name.trim().slice(0, 200).split('/').pop();
67
+ let out = '';
68
+ for (let i = 0; i < base.length; i++) {
69
+ out += isSafeChar(base[i]) ? base[i] : '-';
70
+ }
71
+ let start = 0;
72
+ while (start < out.length && isPadChar(out[start])) start++;
73
+ let end = out.length;
74
+ while (end > start && isPadChar(out[end - 1])) end--;
75
+ const safe = out.slice(start, end);
76
+ return safe || null;
77
+ }
78
+
79
+ module.exports = {
80
+ VALID_LEVELS,
81
+ assertLevel,
82
+ stripAnsi,
83
+ CONTROL_RE,
84
+ sanitizeMessage,
85
+ assertFilenameSafe,
86
+ isSafeChar,
87
+ isPadChar,
88
+ sanitizeAppName,
89
+ };
package/lib/time.js ADDED
@@ -0,0 +1,64 @@
1
+ const dayjs = require('dayjs');
2
+
3
+ const DEFAULT_CONSOLE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
4
+
5
+ function pad2(n) {
6
+ return String(n).padStart(2, '0');
7
+ }
8
+
9
+ function formatDefaultTimestamp(d = new Date()) {
10
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
11
+ }
12
+
13
+ function splitBrackets(fmt) {
14
+ const parts = [];
15
+ let current = '';
16
+ let i = 0;
17
+ while (i < fmt.length) {
18
+ if (fmt[i] === '[') {
19
+ const close = fmt.indexOf(']', i + 1);
20
+ if (close === -1) {
21
+ current += fmt.slice(i);
22
+ break;
23
+ }
24
+ if (current) {
25
+ parts.push(current);
26
+ current = '';
27
+ }
28
+ parts.push(fmt.slice(i, close + 1));
29
+ i = close + 1;
30
+ } else {
31
+ current += fmt[i];
32
+ i++;
33
+ }
34
+ }
35
+ if (current) parts.push(current);
36
+ return parts;
37
+ }
38
+
39
+ function compileTimestampFormatter(fmt) {
40
+ if (typeof fmt !== 'string' || !fmt) return formatDefaultTimestamp;
41
+ if (fmt === DEFAULT_CONSOLE_FORMAT) return formatDefaultTimestamp;
42
+ if (!fmt.includes('[') || !fmt.includes(']')) return () => dayjs().format(fmt);
43
+ const parts = splitBrackets(fmt).map((part) => {
44
+ if (part.startsWith('[') && part.endsWith(']')) {
45
+ const inner = part.slice(1, -1);
46
+ return inner ? () => '[' + dayjs().format(inner) + ']' : () => '[]';
47
+ }
48
+ return () => part;
49
+ });
50
+ return () => parts.map((f) => f()).join('');
51
+ }
52
+
53
+ function formatCustomTimestamp(fmt) {
54
+ return compileTimestampFormatter(fmt)();
55
+ }
56
+
57
+ module.exports = {
58
+ DEFAULT_CONSOLE_FORMAT,
59
+ pad2,
60
+ formatDefaultTimestamp,
61
+ splitBrackets,
62
+ compileTimestampFormatter,
63
+ formatCustomTimestamp,
64
+ };
package/main.js CHANGED
@@ -1,375 +1,136 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const dayjs = require('dayjs');
4
- const { createLogger, format, transports } = require('winston');
5
- require('winston-daily-rotate-file');
6
-
7
- const VALID_LEVELS = ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'];
8
- const DEFAULT_CONSOLE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
9
-
10
- function pad2(n) {
11
- return String(n).padStart(2, '0');
12
- }
13
-
14
- function formatDefaultTimestamp(d = new Date()) {
15
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
16
- }
17
-
18
- function splitBrackets(fmt) {
19
- const parts = [];
20
- let current = '';
21
- let i = 0;
22
- while (i < fmt.length) {
23
- if (fmt[i] === '[') {
24
- const close = fmt.indexOf(']', i + 1);
25
- if (close === -1) {
26
- current += fmt.slice(i);
27
- break;
28
- }
29
- if (current) {
30
- parts.push(current);
31
- current = '';
32
- }
33
- parts.push(fmt.slice(i, close + 1));
34
- i = close + 1;
35
- } else {
36
- current += fmt[i];
37
- i++;
38
- }
39
- }
40
- if (current) parts.push(current);
41
- return parts;
42
- }
43
-
44
- function compileTimestampFormatter(fmt) {
45
- if (typeof fmt !== 'string' || !fmt) return formatDefaultTimestamp;
46
- if (fmt === DEFAULT_CONSOLE_FORMAT) return formatDefaultTimestamp;
47
- if (!fmt.includes('[') || !fmt.includes(']')) return () => dayjs().format(fmt);
48
- const parts = splitBrackets(fmt).map((part) => {
49
- if (part.startsWith('[') && part.endsWith(']')) {
50
- const inner = part.slice(1, -1);
51
- return inner ? () => '[' + dayjs().format(inner) + ']' : () => '[]';
52
- }
53
- return () => part;
54
- });
55
- return () => parts.map((f) => f()).join('');
56
- }
57
-
58
- function formatCustomTimestamp(fmt) {
59
- return compileTimestampFormatter(fmt)();
60
- }
61
-
62
- function formatExtra(meta) {
63
- const keys = Object.keys(meta).filter((k) => k !== 'splat' && k !== 'ms');
64
- if (!keys.length) return '';
65
- try {
66
- const picked = {};
67
- for (const k of keys) picked[k] = meta[k];
68
- return JSON.stringify(picked);
69
- } catch {
70
- return '';
71
- }
72
- }
73
-
74
- const prettyFormat = format.printf(({ timestamp, level, message, ...meta }) => {
75
- const msg = typeof message === 'string' ? message : message == null ? '' : JSON.stringify(message);
76
- const extra = formatExtra(meta);
77
- const body = [msg, extra].filter(Boolean).join(' ');
78
- return timestamp ? `${timestamp} [${level}]: ${body}` : `[${level}]: ${body}`;
79
- });
80
-
81
- function assertLevel(level) {
82
- if (!VALID_LEVELS.includes(level)) throw new Error(`Invalid log level "${level}". Use one of: ${VALID_LEVELS.join(', ')}`);
83
- }
84
-
85
- function stripAnsi(str) {
86
- let out = '';
87
- let i = 0;
88
- while (i < str.length) {
89
- if (str[i] !== '\x1b') {
90
- out += str[i];
91
- i++;
92
- continue;
93
- }
94
- const next = str[i + 1];
95
- if (next === ']') {
96
- let j = i + 2;
97
- while (j < str.length) {
98
- if (str[j] === '\x07') { j++; break; }
99
- if (str[j] === '\x1b' && str[j + 1] === '\\') { j += 2; break; }
100
- j++;
101
- }
102
- i = j;
103
- } else if (next === '[') {
104
- let j = i + 2;
105
- while (j < str.length && str[j] >= ' ' && str[j] <= '?') j++;
106
- if (j < str.length) j++;
107
- i = j;
108
- } else if (next === '(' || next === ')') {
109
- i += 3;
110
- } else {
111
- i += next === undefined ? 1 : 2;
112
- }
113
- }
114
- return out;
115
- }
116
- const CONTROL_RE = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/g;
117
-
118
- function sanitizeMessage(msg) {
119
- if (typeof msg !== 'string') return msg;
120
- return stripAnsi(msg).replace(CONTROL_RE, '');
121
- }
122
-
123
- function assertFilenameSafe(filename) {
124
- if (typeof filename !== 'string' || !filename.trim() || filename.includes('\0')) {
125
- throw new Error('files.filename must be a non-empty string');
126
- }
127
- if (path.isAbsolute(filename) || filename.split(/[\\/]/).includes('..')) {
128
- throw new Error('files.filename must not escape the log folder (.. and absolute paths are rejected)');
129
- }
130
- }
131
-
132
- function isSafeChar(c) {
133
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '.' || c === '_' || c === '-';
134
- }
135
-
136
- function isPadChar(c) {
137
- return c === '-' || c === '_' || c === '.';
138
- }
139
-
140
- function sanitizeAppName(name) {
141
- if (typeof name !== 'string' || !name.trim()) return null;
142
- const base = name.trim().slice(0, 200).split('/').pop();
143
- let out = '';
144
- for (let i = 0; i < base.length; i++) {
145
- out += isSafeChar(base[i]) ? base[i] : '-';
146
- }
147
- let start = 0;
148
- while (start < out.length && isPadChar(out[start])) start++;
149
- let end = out.length;
150
- while (end > start && isPadChar(out[end - 1])) end--;
151
- const safe = out.slice(start, end);
152
- return safe || null;
153
- }
154
-
155
- let cachedAppName = null;
156
- function getDefaultAppName() {
157
- if (cachedAppName) return cachedAppName;
158
- try {
159
- const pkgPath = path.resolve(process.cwd(), 'package.json');
160
- const raw = fs.readFileSync(pkgPath, 'utf8');
161
- cachedAppName = sanitizeAppName(JSON.parse(raw)?.name) ?? 'application';
162
- } catch {
163
- cachedAppName = 'application';
164
- }
165
- return cachedAppName;
166
- }
167
-
168
- function resolveRunNumber(folder, prefix, opt) {
169
- let enabled = false;
170
- let separator = '-';
171
- let padding = 0;
172
- let startAt = 1;
173
- if (opt === true) {
174
- enabled = true;
175
- } else if (opt && typeof opt === 'object') {
176
- enabled = opt.enabled ?? true;
177
- if (opt.separator !== undefined) {
178
- if (typeof opt.separator !== 'string' || !opt.separator) throw new Error('files.runNumber.separator must be a non-empty string');
179
- separator = opt.separator;
180
- }
181
- if (opt.padding !== undefined) {
182
- if (!Number.isInteger(opt.padding) || opt.padding < 0) throw new Error('files.runNumber.padding must be a non-negative integer');
183
- padding = opt.padding;
184
- }
185
- if (opt.startAt !== undefined) {
186
- if (!Number.isInteger(opt.startAt) || opt.startAt < 0) throw new Error('files.runNumber.startAt must be a non-negative integer');
187
- startAt = opt.startAt;
188
- }
189
- } else if (opt !== undefined && opt !== false) {
190
- throw new Error('files.runNumber must be a boolean or an object');
191
- }
192
- if (!enabled) return null;
193
- const counterFile = path.join(folder, `.${prefix}.run`);
194
- let next = startAt;
195
- try {
196
- const raw = fs.readFileSync(counterFile, 'utf8').trim();
197
- const prev = parseInt(raw, 10);
198
- next = Number.isInteger(prev) ? prev + 1 : startAt;
199
- } catch {}
200
- try {
201
- fs.writeFileSync(counterFile, String(next), 'utf8');
202
- } catch {}
203
- return separator + String(next).padStart(padding, '0');
204
- }
205
-
206
- function normalizeFilesOptions(files = {}) {
207
- const rawFolder = files.folder ?? 'logs';
208
- if (typeof rawFolder !== 'string' || !rawFolder.trim() || rawFolder.includes('\0')) {
209
- throw new Error('files.folder must be a non-empty string');
210
- }
211
- const folder = path.normalize(rawFolder);
212
- const datePattern = files.datePattern ?? files.dateFormat ?? files.filesName ?? 'YYYY_MM_DD';
213
- if (typeof datePattern !== 'string' || !datePattern.trim()) {
214
- throw new Error('files.datePattern must be a non-empty string');
215
- }
216
- const explicitPattern = files.filePattern ?? files.pattern ?? files.filename;
217
- const explicitPrefix = files.prefix ?? files.appName;
218
- if (explicitPattern !== undefined && explicitPrefix !== undefined) {
219
- throw new Error('Do not combine "prefix" with "filePattern", pick one: prefix builds "<prefix>-%DATE%.log" for you, filePattern takes full control');
220
- }
221
- let filename = explicitPattern ?? `${sanitizeAppName(explicitPrefix) ?? getDefaultAppName()}-%DATE%.log`;
222
- filename = filename.split('%datePattern%').join('%DATE%');
223
- assertFilenameSafe(filename);
224
- if (!filename.includes('%DATE%')) {
225
- const ext = path.extname(filename) || '.log';
226
- const base = path.basename(filename, ext);
227
- filename = `${base}-%DATE%${ext}`;
228
- }
229
- assertFilenameSafe(filename);
230
- if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
231
- const rawPrefix = path.basename(filename).split('%DATE%')[0];
232
- let prefixEnd = rawPrefix.length;
233
- while (prefixEnd > 0 && isPadChar(rawPrefix[prefixEnd - 1])) prefixEnd--;
234
- const prefix = rawPrefix.slice(0, prefixEnd) || 'application';
235
- const runSuffix = resolveRunNumber(folder, sanitizeAppName(prefix) ?? 'application', files.runNumber ?? false);
236
- if (runSuffix) {
237
- const ext = path.extname(filename) || '.log';
238
- const base = filename.slice(0, -ext.length);
239
- filename = `${base}${runSuffix}${ext}`;
240
- }
241
- return {
242
- folder,
243
- datePattern,
244
- filename,
245
- maxFiles: files.maxFile ?? files.maxFiles ?? '14d',
246
- maxSize: files.maxSize ?? '20m',
247
- zippedArchive: files.zippedArchive ?? false,
248
- level: files.level,
249
- };
250
- }
251
-
252
- function buildConsoleTransport({ enableTimestamp = true, timestampFormat = DEFAULT_CONSOLE_FORMAT, timestampPrefix = '', timestampSuffix = '', colorize = true, level } = {}) {
253
- const parts = [];
254
- if (colorize) parts.push(format.colorize());
255
- if (enableTimestamp) {
256
- const stamp = compileTimestampFormatter(timestampFormat);
257
- parts.push(format.timestamp({ format: () => `${timestampPrefix}${stamp()}${timestampSuffix}` }));
258
- } else {
259
- parts.push(format((info) => { delete info.timestamp; return info; })());
260
- }
261
- parts.push(format.errors({ stack: true }), format.splat(), prettyFormat);
262
- return new transports.Console({ format: format.combine(...parts), level });
263
- }
264
-
265
- function buildFileTransport({ folder, datePattern, filename, maxFiles, maxSize, zippedArchive, level }) {
266
- if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
267
- return new transports.DailyRotateFile({
268
- filename: path.join(folder, filename),
269
- datePattern,
270
- zippedArchive,
271
- maxSize,
272
- maxFiles,
273
- level,
274
- format: format.combine(format.errors({ stack: true }), format.splat(), format.timestamp(), prettyFormat),
275
- });
276
- }
277
-
278
- function createLoggerInstance(initial = {}) {
279
- const globalLevel = process.env.LOG_LEVEL || initial.level || 'debug';
280
- assertLevel(globalLevel);
281
-
282
- const logger = createLogger({
283
- level: globalLevel,
284
- format: format.combine(format.errors({ stack: true }), format.splat()),
285
- });
286
-
287
- let consoleTransport = buildConsoleTransport(initial.console);
288
- consoleTransport.level = initial.console?.level;
289
- logger.add(consoleTransport);
290
-
291
- let fileTransport = null;
292
- let consoleOpts = { ...(initial.console || {}) };
293
- let filesOpts = null;
294
-
295
- function applySettings(options = {}) {
296
- if (options.level !== undefined) {
297
- assertLevel(options.level);
298
- logger.level = options.level;
299
- }
300
- if (options.console) {
301
- if (options.console.level !== undefined && options.console.level !== null) assertLevel(options.console.level);
302
- consoleOpts = { ...consoleOpts, ...options.console };
303
- logger.remove(consoleTransport);
304
- consoleTransport = buildConsoleTransport(consoleOpts);
305
- consoleTransport.level = consoleOpts.level;
306
- logger.add(consoleTransport);
307
- }
308
- if (options.files) {
309
- const normalized = normalizeFilesOptions(options.files);
310
- if (normalized.level !== undefined && normalized.level !== null) assertLevel(normalized.level);
311
- filesOpts = normalized;
312
- if (fileTransport) logger.remove(fileTransport);
313
- fileTransport = buildFileTransport(normalized);
314
- logger.add(fileTransport);
315
- }
316
- }
317
-
318
- function isEnabled(level) {
319
- const want = logger.levels[level];
320
- const current = logger.levels[logger.level];
321
- return want !== undefined && current !== undefined && want <= current;
322
- }
323
-
324
- function write(level, message, ...meta) {
325
- if (typeof message === 'function') {
326
- if (!isEnabled(level)) return;
327
- message = message();
328
- }
329
- if (message instanceof Error) {
330
- logger.log(level, sanitizeMessage(message.message), { stack: message.stack, ...meta[0] });
331
- return;
332
- }
333
- logger.log(level, sanitizeMessage(message), ...meta);
334
- }
335
-
336
- function callable(message, ...meta) {
337
- write('info', message, ...meta);
338
- }
339
-
340
- callable.error = (m, ...a) => write('error', m, ...a);
341
- callable.warn = (m, ...a) => write('warn', m, ...a);
342
- callable.info = (m, ...a) => write('info', m, ...a);
343
- callable.debug = (m, ...a) => write('debug', m, ...a);
344
- callable.http = (m, ...a) => write('http', m, ...a);
345
- callable.verbose = (m, ...a) => write('verbose', m, ...a);
346
- callable.silly = (m, ...a) => write('silly', m, ...a);
347
- callable.log = (level, m, ...a) => { assertLevel(level); write(level, m, ...a); };
348
-
349
- callable.settings = applySettings;
350
- callable.setLevel = (level) => { assertLevel(level); logger.level = level; };
351
- callable.getLevel = () => logger.level;
352
- callable.child = (meta = {}) => {
353
- const child = logger.child(meta);
354
- return {
355
- error: (m, ...a) => child.error(m, ...a),
356
- warn: (m, ...a) => child.warn(m, ...a),
357
- info: (m, ...a) => child.info(m, ...a),
358
- debug: (m, ...a) => child.debug(m, ...a),
359
- log: child.log.bind(child),
360
- };
361
- };
362
- callable.close = () => {
363
- if (fileTransport) { logger.remove(fileTransport); try { fileTransport.close?.(); } catch {} fileTransport = null; }
364
- };
365
- callable.createLogger = (opts = {}) => createLoggerInstance(opts);
366
- callable._winston = logger;
367
-
368
- if (initial.files) applySettings({ files: initial.files });
369
- if (initial.console || initial.level) applySettings({ console: initial.console, level: initial.level });
370
-
371
- return callable;
372
- }
373
-
374
- module.exports = createLoggerInstance();
375
- module.exports.createLogger = createLoggerInstance;
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { MiniLogger, ConsoleTransport, NullTransport, colorizeLevel, resolveLevelColors } = require('./lib/mini');
4
+ const { FileTransport } = require('./lib/rotate');
5
+ const { DEFAULT_CONSOLE_FORMAT, compileTimestampFormatter } = require('./lib/time');
6
+ const { buildLine } = require('./lib/format');
7
+ const { assertLevel, sanitizeMessage } = require('./lib/sanitize');
8
+ const { normalizeFilesOptions } = require('./lib/files');
9
+
10
+ function normalizeConsoleOpts(input) {
11
+ if (input === false) return { enabled: false };
12
+ return input || {};
13
+ }
14
+
15
+ function buildConsoleTransport({ enabled = true, enableTimestamp = true, timestampFormat = DEFAULT_CONSOLE_FORMAT, timestampPrefix = '', timestampSuffix = '', colorize = true, colors, level } = {}) {
16
+ if (!enabled) return new NullTransport();
17
+ const stamp = enableTimestamp ? compileTimestampFormatter(timestampFormat) : null;
18
+ const palette = resolveLevelColors(colors);
19
+ const render = ({ level, message, meta }) => {
20
+ const timestamp = stamp ? `${timestampPrefix}${stamp()}${timestampSuffix}` : '';
21
+ const tag = colorize ? colorizeLevel(level, `[${level}]`, palette) : `[${level}]`;
22
+ return buildLine(timestamp, level, message, meta, tag);
23
+ };
24
+ return new ConsoleTransport({ level, render });
25
+ }
26
+
27
+ function buildFileTransport({ folder, datePattern, filename, maxFiles, maxSize, zippedArchive, level }) {
28
+ if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
29
+ return new FileTransport({
30
+ folder: path.normalize(folder),
31
+ filename,
32
+ datePattern,
33
+ zippedArchive,
34
+ maxSize,
35
+ maxFiles,
36
+ level,
37
+ render: ({ level, message, meta }) => buildLine(new Date().toISOString(), level, message, meta),
38
+ });
39
+ }
40
+
41
+ function createLoggerInstance(initial = {}) {
42
+ const globalLevel = process.env.LOG_LEVEL || initial.level || 'debug';
43
+ assertLevel(globalLevel);
44
+
45
+ const logger = new MiniLogger({ level: globalLevel });
46
+
47
+ let consoleTransport = buildConsoleTransport(normalizeConsoleOpts(initial.console));
48
+ consoleTransport.level = normalizeConsoleOpts(initial.console).level;
49
+ logger.add(consoleTransport);
50
+
51
+ let fileTransport = null;
52
+ let consoleOpts = { ...normalizeConsoleOpts(initial.console) };
53
+ let filesOpts = null;
54
+
55
+ function applySettings(options = {}) {
56
+ if (options.level !== undefined) {
57
+ assertLevel(options.level);
58
+ logger.level = options.level;
59
+ }
60
+ if (options.console !== undefined) {
61
+ const next = normalizeConsoleOpts(options.console);
62
+ if (next.level !== undefined && next.level !== null) assertLevel(next.level);
63
+ consoleOpts = { ...consoleOpts, ...next };
64
+ logger.remove(consoleTransport);
65
+ consoleTransport = buildConsoleTransport(consoleOpts);
66
+ consoleTransport.level = consoleOpts.level;
67
+ logger.add(consoleTransport);
68
+ }
69
+ if (options.files) {
70
+ const normalized = normalizeFilesOptions(options.files);
71
+ if (normalized.level !== undefined && normalized.level !== null) assertLevel(normalized.level);
72
+ filesOpts = normalized;
73
+ if (fileTransport) logger.remove(fileTransport);
74
+ fileTransport = buildFileTransport(normalized);
75
+ logger.add(fileTransport);
76
+ }
77
+ }
78
+
79
+ function isEnabled(level) {
80
+ const want = logger.levels[level];
81
+ const current = logger.levels[logger.level];
82
+ return want !== undefined && current !== undefined && want <= current;
83
+ }
84
+
85
+ function write(level, message, ...meta) {
86
+ if (typeof message === 'function') {
87
+ if (!isEnabled(level)) return;
88
+ message = message();
89
+ }
90
+ if (message instanceof Error) {
91
+ logger.log(level, sanitizeMessage(message.message), { stack: message.stack, ...meta[0] });
92
+ return;
93
+ }
94
+ logger.log(level, sanitizeMessage(message), ...meta);
95
+ }
96
+
97
+ function callable(message, ...meta) {
98
+ write('info', message, ...meta);
99
+ }
100
+
101
+ callable.error = (m, ...a) => write('error', m, ...a);
102
+ callable.warn = (m, ...a) => write('warn', m, ...a);
103
+ callable.info = (m, ...a) => write('info', m, ...a);
104
+ callable.debug = (m, ...a) => write('debug', m, ...a);
105
+ callable.http = (m, ...a) => write('http', m, ...a);
106
+ callable.verbose = (m, ...a) => write('verbose', m, ...a);
107
+ callable.silly = (m, ...a) => write('silly', m, ...a);
108
+ callable.log = (level, m, ...a) => { assertLevel(level); write(level, m, ...a); };
109
+
110
+ callable.settings = applySettings;
111
+ callable.setLevel = (level) => { assertLevel(level); logger.level = level; };
112
+ callable.getLevel = () => logger.level;
113
+ callable.child = (meta = {}) => {
114
+ const child = logger.child(meta);
115
+ return {
116
+ error: (m, ...a) => child.error(m, ...a),
117
+ warn: (m, ...a) => child.warn(m, ...a),
118
+ info: (m, ...a) => child.info(m, ...a),
119
+ debug: (m, ...a) => child.debug(m, ...a),
120
+ log: child.log.bind(child),
121
+ };
122
+ };
123
+ callable.close = () => {
124
+ if (fileTransport) { logger.remove(fileTransport); try { fileTransport.close?.(); } catch {} fileTransport = null; }
125
+ };
126
+ callable.createLogger = (opts = {}) => createLoggerInstance(opts);
127
+ callable._winston = logger;
128
+
129
+ if (initial.files) applySettings({ files: initial.files });
130
+ if (initial.console || initial.level) applySettings({ console: initial.console, level: initial.level });
131
+
132
+ return callable;
133
+ }
134
+
135
+ module.exports = createLoggerInstance();
136
+ module.exports.createLogger = createLoggerInstance;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logxpert",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "A powerful logging library for Node.js with colorful output and daily file rotation",
5
5
  "main": "main.js",
6
6
  "types": "index.d.ts",
@@ -22,6 +22,7 @@
22
22
  "main.mjs",
23
23
  "index.d.ts",
24
24
  "index.d.mts",
25
+ "lib/",
25
26
  "README.md",
26
27
  "LICENSE",
27
28
  "CHANGELOG.md"
@@ -47,14 +48,13 @@
47
48
  "node": ">=18"
48
49
  },
49
50
  "scripts": {
50
- "test": "node --test test/logger.test.js",
51
- "check:types": "tsc -p test-d --noEmit",
51
+ "test": "node --test --test-reporter=./test/reporter.js test/logger.test.js test/console.test.js test/files.test.js test/lib.test.js",
52
+ "test:demo": "node --test --test-reporter=./test/reporter.js demo/reporter.demo.test.js",
53
+ "check:types": "tsc -p test-d --noEmit && echo TYPES-OK",
52
54
  "attw": "attw --pack"
53
55
  },
54
56
  "dependencies": {
55
- "dayjs": "^1.11.23",
56
- "winston": "^3.19.0",
57
- "winston-daily-rotate-file": "^5.0.0"
57
+ "dayjs": "^1.11.23"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@arethetypeswrong/cli": "^0.18.5",