logxpert 2.0.0 → 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/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,100 +1,40 @@
1
1
  const fs = require('fs');
2
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 formatCustomTimestamp(fmt) {
11
- if (typeof fmt !== 'string' || !fmt) return dayjs().format(DEFAULT_CONSOLE_FORMAT);
12
- if (fmt.includes('[') && fmt.includes(']')) {
13
- return fmt.split(/(\[[^\]]*\])/).map((part) => {
14
- if (part.startsWith('[') && part.endsWith(']')) {
15
- const inner = part.slice(1, -1);
16
- return '[' + (inner ? dayjs().format(inner) : '') + ']';
17
- }
18
- return part;
19
- }).join('');
20
- }
21
- return dayjs().format(fmt);
22
- }
23
-
24
- function formatExtra(meta) {
25
- const keys = Object.keys(meta).filter((k) => k !== 'splat' && k !== 'ms');
26
- if (!keys.length) return '';
27
- try {
28
- const picked = {};
29
- for (const k of keys) picked[k] = meta[k];
30
- return JSON.stringify(picked);
31
- } catch {
32
- return '';
33
- }
34
- }
35
-
36
- const prettyFormat = format.printf(({ timestamp, level, message, ...meta }) => {
37
- const msg = typeof message === 'string' ? message : message == null ? '' : JSON.stringify(message);
38
- const extra = formatExtra(meta);
39
- const body = [msg, extra].filter(Boolean).join(' ');
40
- return timestamp ? `${timestamp} [${level}]: ${body}` : `[${level}]: ${body}`;
41
- });
42
-
43
- function assertLevel(level) {
44
- if (!VALID_LEVELS.includes(level)) throw new Error(`Invalid log level "${level}". Use one of: ${VALID_LEVELS.join(', ')}`);
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 || {};
45
13
  }
46
14
 
47
- function normalizeFilesOptions(files = {}) {
48
- const folder = files.folder ?? 'logs';
49
- if (typeof folder !== 'string' || !folder.trim() || folder.includes('\0')) {
50
- throw new Error('files.folder must be a non-empty string');
51
- }
52
- const datePattern = files.filesName ?? files.datePattern ?? 'YYYY-MM-DD';
53
- if (typeof datePattern !== 'string' || !datePattern.trim()) {
54
- throw new Error('files.filesName/datePattern must be a non-empty string');
55
- }
56
- let filename = files.filename ?? `application-%DATE%.log`;
57
- if (typeof filename !== 'string' || !filename.trim()) {
58
- throw new Error('files.filename must be a non-empty string');
59
- }
60
- if (!filename.includes('%DATE%')) {
61
- const ext = path.extname(filename) || '.log';
62
- const base = path.basename(filename, ext);
63
- filename = `${base}-%DATE%${ext}`;
64
- }
65
- return {
66
- folder: path.normalize(folder),
67
- datePattern,
68
- filename,
69
- maxFiles: files.maxFile ?? files.maxFiles ?? '14d',
70
- maxSize: files.maxSize ?? '20m',
71
- zippedArchive: files.zippedArchive ?? false,
72
- level: files.level,
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);
73
23
  };
74
- }
75
-
76
- function buildConsoleTransport({ enableTimestamp = true, timestampFormat = DEFAULT_CONSOLE_FORMAT, timestampPrefix = '', timestampSuffix = '', colorize = true, level } = {}) {
77
- const parts = [];
78
- if (colorize) parts.push(format.colorize());
79
- if (enableTimestamp) {
80
- parts.push(format.timestamp({ format: () => `${timestampPrefix}${formatCustomTimestamp(timestampFormat)}${timestampSuffix}` }));
81
- } else {
82
- parts.push(format((info) => { delete info.timestamp; return info; })());
83
- }
84
- parts.push(format.errors({ stack: true }), format.splat(), prettyFormat);
85
- return new transports.Console({ format: format.combine(...parts), level });
24
+ return new ConsoleTransport({ level, render });
86
25
  }
87
26
 
88
27
  function buildFileTransport({ folder, datePattern, filename, maxFiles, maxSize, zippedArchive, level }) {
89
28
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
90
- return new transports.DailyRotateFile({
91
- filename: path.join(folder, filename),
29
+ return new FileTransport({
30
+ folder: path.normalize(folder),
31
+ filename,
92
32
  datePattern,
93
33
  zippedArchive,
94
34
  maxSize,
95
35
  maxFiles,
96
36
  level,
97
- format: format.combine(format.errors({ stack: true }), format.splat(), format.timestamp(), prettyFormat),
37
+ render: ({ level, message, meta }) => buildLine(new Date().toISOString(), level, message, meta),
98
38
  });
99
39
  }
100
40
 
@@ -102,17 +42,14 @@ function createLoggerInstance(initial = {}) {
102
42
  const globalLevel = process.env.LOG_LEVEL || initial.level || 'debug';
103
43
  assertLevel(globalLevel);
104
44
 
105
- const logger = createLogger({
106
- level: globalLevel,
107
- format: format.combine(format.errors({ stack: true }), format.splat()),
108
- });
45
+ const logger = new MiniLogger({ level: globalLevel });
109
46
 
110
- let consoleTransport = buildConsoleTransport(initial.console);
111
- consoleTransport.level = initial.console?.level;
47
+ let consoleTransport = buildConsoleTransport(normalizeConsoleOpts(initial.console));
48
+ consoleTransport.level = normalizeConsoleOpts(initial.console).level;
112
49
  logger.add(consoleTransport);
113
50
 
114
51
  let fileTransport = null;
115
- let consoleOpts = { ...(initial.console || {}) };
52
+ let consoleOpts = { ...normalizeConsoleOpts(initial.console) };
116
53
  let filesOpts = null;
117
54
 
118
55
  function applySettings(options = {}) {
@@ -120,9 +57,10 @@ function createLoggerInstance(initial = {}) {
120
57
  assertLevel(options.level);
121
58
  logger.level = options.level;
122
59
  }
123
- if (options.console) {
124
- if (options.console.level !== undefined && options.console.level !== null) assertLevel(options.console.level);
125
- consoleOpts = { ...consoleOpts, ...options.console };
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 };
126
64
  logger.remove(consoleTransport);
127
65
  consoleTransport = buildConsoleTransport(consoleOpts);
128
66
  consoleTransport.level = consoleOpts.level;
@@ -138,12 +76,22 @@ function createLoggerInstance(initial = {}) {
138
76
  }
139
77
  }
140
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
+
141
85
  function write(level, message, ...meta) {
86
+ if (typeof message === 'function') {
87
+ if (!isEnabled(level)) return;
88
+ message = message();
89
+ }
142
90
  if (message instanceof Error) {
143
- logger.log(level, message.message, { stack: message.stack, ...meta[0] });
91
+ logger.log(level, sanitizeMessage(message.message), { stack: message.stack, ...meta[0] });
144
92
  return;
145
93
  }
146
- logger.log(level, message, ...meta);
94
+ logger.log(level, sanitizeMessage(message), ...meta);
147
95
  }
148
96
 
149
97
  function callable(message, ...meta) {
package/package.json CHANGED
@@ -1,51 +1,63 @@
1
- {
2
- "name": "logxpert",
3
- "version": "2.0.0",
4
- "description": "A powerful logging library for Node.js with colorful output and daily file rotation",
5
- "main": "main.js",
6
- "types": "index.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./index.d.ts",
10
- "import": "./main.mjs",
11
- "require": "./main.js"
12
- },
13
- "./package.json": "./package.json"
14
- },
15
- "files": [
16
- "main.js",
17
- "main.mjs",
18
- "index.d.ts",
19
- "README.md",
20
- "LICENSE",
21
- "CHANGELOG.md"
22
- ],
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/lighscent/LogXpert.git"
26
- },
27
- "bugs": {
28
- "url": "https://github.com/lighscent/LogXpert/issues"
29
- },
30
- "homepage": "https://github.com/lighscent/LogXpert#readme",
31
- "author": "https://github.com/lighscent",
32
- "license": "GPL-3.0-only",
33
- "keywords": [
34
- "logging",
35
- "logger",
36
- "winston",
37
- "daily-rotate",
38
- "nodejs"
39
- ],
40
- "engines": {
41
- "node": ">=18"
42
- },
43
- "scripts": {
44
- "test": "node --test test/logger.test.js"
45
- },
46
- "dependencies": {
47
- "dayjs": "^1.11.23",
48
- "winston": "^3.19.0",
49
- "winston-daily-rotate-file": "^5.0.0"
50
- }
51
- }
1
+ {
2
+ "name": "logxpert",
3
+ "version": "2.1.0",
4
+ "description": "A powerful logging library for Node.js with colorful output and daily file rotation",
5
+ "main": "main.js",
6
+ "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": {
10
+ "types": "./index.d.mts",
11
+ "default": "./main.mjs"
12
+ },
13
+ "require": {
14
+ "types": "./index.d.ts",
15
+ "default": "./main.js"
16
+ }
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "main.js",
22
+ "main.mjs",
23
+ "index.d.ts",
24
+ "index.d.mts",
25
+ "lib/",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/lighscent/LogXpert.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/lighscent/LogXpert/issues"
36
+ },
37
+ "homepage": "https://github.com/lighscent/LogXpert#readme",
38
+ "author": "https://github.com/lighscent",
39
+ "license": "Apache-2.0",
40
+ "keywords": [
41
+ "logging",
42
+ "logger",
43
+ "winston",
44
+ "daily-rotate",
45
+ "nodejs"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "scripts": {
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",
54
+ "attw": "attw --pack"
55
+ },
56
+ "dependencies": {
57
+ "dayjs": "^1.11.23"
58
+ },
59
+ "devDependencies": {
60
+ "@arethetypeswrong/cli": "^0.18.5",
61
+ "typescript": "^7.0.2"
62
+ }
63
+ }