@arkv/logger 0.1.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 ADDED
@@ -0,0 +1,200 @@
1
+ # @arkv/logger
2
+
3
+ Framework-agnostic structured logger with async context, sanitization, and colored output.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @arkv/logger
9
+ # or
10
+ npm install @arkv/logger
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Basic Logging
16
+
17
+ ```typescript
18
+ import { Logger } from '@arkv/logger';
19
+
20
+ const logger = new Logger({
21
+ name: 'my-app',
22
+ version: '1.0.0',
23
+ env: 'production',
24
+ });
25
+
26
+ logger.log('Server started');
27
+ logger.debug('Loading config', { path: '/etc/app' });
28
+ logger.warn('Disk usage high', { usage: 92 });
29
+ logger.error('Request failed', new Error('timeout'));
30
+ ```
31
+
32
+ ### Log Levels
33
+
34
+ Six levels in ascending severity:
35
+
36
+ ```typescript
37
+ import { Logger, LogLevel } from '@arkv/logger';
38
+
39
+ const logger = new Logger({ level: LogLevel.WARN });
40
+
41
+ logger.debug('skipped'); // below WARN, not logged
42
+ logger.warn('logged'); // WARN and above are logged
43
+ logger.error('logged');
44
+ logger.fatal('logged');
45
+ ```
46
+
47
+ | Level | Value |
48
+ |-------|-------|
49
+ | `LogLevel.VERBOSE` | `'verbose'` |
50
+ | `LogLevel.DEBUG` | `'debug'` |
51
+ | `LogLevel.LOG` | `'log'` |
52
+ | `LogLevel.WARN` | `'warn'` |
53
+ | `LogLevel.ERROR` | `'error'` |
54
+ | `LogLevel.FATAL` | `'fatal'` |
55
+
56
+ ### Async Context
57
+
58
+ Track request-scoped data across async boundaries using `ContextStore` (backed by `AsyncLocalStorage`):
59
+
60
+ ```typescript
61
+ import { Logger, ContextStore } from '@arkv/logger';
62
+
63
+ const context = new ContextStore();
64
+ const logger = new Logger({ name: 'api' }, context);
65
+
66
+ function handleRequest(req) {
67
+ context.runWithContext(
68
+ { requestId: req.id, userId: req.user },
69
+ () => {
70
+ // requestId and userId are automatically
71
+ // included in every log entry
72
+ logger.log('Processing request');
73
+ doWork();
74
+ },
75
+ );
76
+ }
77
+ ```
78
+
79
+ ### Sensitive Field Masking
80
+
81
+ Fields matching known sensitive names are automatically replaced with `[MASKED]`:
82
+
83
+ ```typescript
84
+ logger.log('User login', {
85
+ username: 'alice',
86
+ password: 'secret', // → [MASKED]
87
+ token: 'jwt-abc', // → [MASKED]
88
+ apiKey: 'key-123', // → [MASKED]
89
+ });
90
+ ```
91
+
92
+ Default masked fields: `password`, `secret`, `token`, `authorization`, `cookie`, `apiKey`, `apiSecret`, `apiPass`. Add custom fields via config:
93
+
94
+ ```typescript
95
+ const logger = new Logger({
96
+ maskFields: ['ssn', 'creditCard'],
97
+ });
98
+ ```
99
+
100
+ ### Error Handling
101
+
102
+ Errors are automatically extracted and serialized from multiple patterns:
103
+
104
+ ```typescript
105
+ // Direct Error object
106
+ logger.error('Failed', new Error('timeout'));
107
+
108
+ // Error as message
109
+ logger.error(new Error('crash'));
110
+
111
+ // Wrapped in object
112
+ logger.error('Op failed', { err: new Error('db') });
113
+ logger.error('Op failed', { error: new Error('db') });
114
+
115
+ // String shorthand at error/warn/fatal level
116
+ logger.error('Op failed', { error: 'connection refused' });
117
+
118
+ // Deeply nested errors are found automatically
119
+ logger.error('Op failed', {
120
+ metadata: { nested: { err: new Error('deep') } },
121
+ });
122
+ ```
123
+
124
+ ### Development vs Production Output
125
+
126
+ In development (`NODE_ENV !== 'production'`), output is colored JSON for readability. In production, output is plain JSON for log aggregators:
127
+
128
+ ```typescript
129
+ // Colored dev output (default)
130
+ const dev = new Logger({ isDevelopment: true });
131
+
132
+ // Plain JSON for production
133
+ const prod = new Logger({ isDevelopment: false });
134
+ ```
135
+
136
+ ### Event Filtering
137
+
138
+ Suppress logs for specific events (e.g. health checks):
139
+
140
+ ```typescript
141
+ const logger = new Logger(
142
+ { filterEvents: ['/health', '/ready'] },
143
+ contextStore,
144
+ );
145
+ ```
146
+
147
+ When the context's `event` field matches a filtered event, the log is silently dropped.
148
+
149
+ ## API
150
+
151
+ ### `Logger`
152
+
153
+ ```typescript
154
+ new Logger(config?: LoggerConfig, context?: ContextStore)
155
+ ```
156
+
157
+ | Property | Type | Description |
158
+ |----------|------|-------------|
159
+ | `logLevel` | `LogLevel` | Current log level (read-only) |
160
+ | `appId` | `string \| undefined` | `name-version-env` or `undefined` |
161
+
162
+ | Method | Description |
163
+ |--------|-------------|
164
+ | `log(message, ...params)` | Log at `log` level |
165
+ | `debug(message, ...params)` | Log at `debug` level |
166
+ | `verbose(message, ...params)` | Log at `verbose` level |
167
+ | `warn(message, ...params)` | Log at `warn` level |
168
+ | `error(message, ...params)` | Log at `error` level |
169
+ | `fatal(message, ...params)` | Log at `fatal` level |
170
+
171
+ Each method accepts `string`, `Record<string, unknown>`, or `Error` as the message, plus optional extra params.
172
+
173
+ ### `LoggerConfig`
174
+
175
+ | Field | Type | Default | Description |
176
+ |-------|------|---------|-------------|
177
+ | `name` | `string` | — | Application name |
178
+ | `version` | `string` | — | Application version |
179
+ | `env` | `string` | — | Environment name |
180
+ | `level` | `LogLevel` | `DEBUG` | Minimum log level |
181
+ | `isDevelopment` | `boolean` | `NODE_ENV !== 'production'` | Colored vs plain JSON output |
182
+ | `maskFields` | `string[]` | `[]` | Additional fields to mask (merged with defaults) |
183
+ | `filterEvents` | `string[]` | `[]` | Context events to suppress |
184
+ | `maxArrayLength` | `number` | `100` | Max array items before truncation |
185
+
186
+ ### `ContextStore`
187
+
188
+ ```typescript
189
+ new ContextStore()
190
+ ```
191
+
192
+ | Method | Description |
193
+ |--------|-------------|
194
+ | `getContext()` | Get current async context |
195
+ | `updateContext(partial)` | Merge partial update into current context |
196
+ | `runWithContext(ctx, callback)` | Execute callback within a context |
197
+
198
+ ## License
199
+
200
+ [MIT](../../LICENSE)
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContextStore = void 0;
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ class ContextStore {
6
+ asyncLocalStorage = new node_async_hooks_1.AsyncLocalStorage();
7
+ getContext() {
8
+ const context = this.asyncLocalStorage.getStore();
9
+ if (!context) {
10
+ return {};
11
+ }
12
+ return { ...context };
13
+ }
14
+ updateContext(obj) {
15
+ const context = this.asyncLocalStorage.getStore();
16
+ if (context) {
17
+ Object.assign(context, obj);
18
+ }
19
+ }
20
+ runWithContext(context, callback) {
21
+ return this.asyncLocalStorage.run(context, callback);
22
+ }
23
+ }
24
+ exports.ContextStore = ContextStore;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatColoredJson = formatColoredJson;
4
+ const colors_1 = require("@arkv/colors");
5
+ const shared_1 = require("@arkv/shared");
6
+ function formatColoredJson(obj, level) {
7
+ const jsonString = (0, shared_1.safeStringify)(obj);
8
+ const colorMap = {
9
+ level: (0, colors_1.getLevelColorFn)(level),
10
+ message: colors_1.green,
11
+ timestamp: colors_1.magenta,
12
+ requestId: colors_1.brightGreen,
13
+ userId: colors_1.brightBlue,
14
+ context: colors_1.brightCyan,
15
+ duration: colors_1.yellow,
16
+ event: colors_1.brightMagenta,
17
+ error: colors_1.red,
18
+ exception: colors_1.red,
19
+ flow: colors_1.brightGreen,
20
+ method: colors_1.brightBlue,
21
+ stack: colors_1.gray,
22
+ status: colors_1.brightYellow,
23
+ elapsed: colors_1.brightYellow,
24
+ };
25
+ return jsonString.replace(/(".*?":\s*)(.*?)(?=,|\n|$)/g, (_, key, value) => {
26
+ const keyWithoutQuotes = key
27
+ .replace(/"/g, '')
28
+ .slice(0, -1);
29
+ const colorizer = colorMap[keyWithoutQuotes] || (0, colors_1.getValueColor)(value);
30
+ return `${(0, colors_1.cyan)(key)}${colorizer(value)}`;
31
+ });
32
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogLevel = exports.LOG_LEVELS = exports.DEFAULT_MASK_FIELDS = exports.Logger = exports.ContextStore = void 0;
4
+ var context_js_1 = require("./context.js");
5
+ Object.defineProperty(exports, "ContextStore", { enumerable: true, get: function () { return context_js_1.ContextStore; } });
6
+ var logger_js_1 = require("./logger.js");
7
+ Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_js_1.Logger; } });
8
+ var types_js_1 = require("./types.js");
9
+ Object.defineProperty(exports, "DEFAULT_MASK_FIELDS", { enumerable: true, get: function () { return types_js_1.DEFAULT_MASK_FIELDS; } });
10
+ Object.defineProperty(exports, "LOG_LEVELS", { enumerable: true, get: function () { return types_js_1.LOG_LEVELS; } });
11
+ Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return types_js_1.LogLevel; } });
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Logger = void 0;
4
+ const shared_1 = require("@arkv/shared");
5
+ const format_js_1 = require("./format.js");
6
+ const sanitize_js_1 = require("./sanitize.js");
7
+ const types_js_1 = require("./types.js");
8
+ class Logger {
9
+ logLevel;
10
+ #isDevelopment;
11
+ #maskFields;
12
+ #maxArrayLength;
13
+ #filterEvents;
14
+ #context;
15
+ #appName;
16
+ #appVersion;
17
+ #appEnv;
18
+ constructor(config, context) {
19
+ const cfg = config ?? {};
20
+ this.logLevel = cfg.level ?? types_js_1.LogLevel.DEBUG;
21
+ this.#isDevelopment =
22
+ cfg.isDevelopment ??
23
+ process.env.NODE_ENV !== 'production';
24
+ this.#maskFields =
25
+ cfg.maskFields && cfg.maskFields.length > 0
26
+ ? Array.from(new Set([
27
+ ...types_js_1.DEFAULT_MASK_FIELDS,
28
+ ...cfg.maskFields,
29
+ ]))
30
+ : [...types_js_1.DEFAULT_MASK_FIELDS];
31
+ this.#maxArrayLength = cfg.maxArrayLength ?? 100;
32
+ this.#filterEvents = cfg.filterEvents ?? [];
33
+ this.#context = context;
34
+ this.#appName = cfg.name;
35
+ this.#appVersion = cfg.version;
36
+ this.#appEnv = cfg.env;
37
+ }
38
+ get appId() {
39
+ if (this.#appName && this.#appVersion && this.#appEnv) {
40
+ return `${this.#appName}-${this.#appVersion}-${this.#appEnv}`;
41
+ }
42
+ return undefined;
43
+ }
44
+ log(message, ...optionalParams) {
45
+ this.#writeLog(types_js_1.LogLevel.LOG, message, optionalParams);
46
+ }
47
+ error(message, ...optionalParams) {
48
+ this.#writeLog(types_js_1.LogLevel.ERROR, message, optionalParams);
49
+ }
50
+ warn(message, ...optionalParams) {
51
+ this.#writeLog(types_js_1.LogLevel.WARN, message, optionalParams);
52
+ }
53
+ debug(message, ...optionalParams) {
54
+ this.#writeLog(types_js_1.LogLevel.DEBUG, message, optionalParams);
55
+ }
56
+ verbose(message, ...optionalParams) {
57
+ this.#writeLog(types_js_1.LogLevel.VERBOSE, message, optionalParams);
58
+ }
59
+ fatal(message, ...optionalParams) {
60
+ this.#writeLog(types_js_1.LogLevel.FATAL, message, optionalParams);
61
+ }
62
+ #writeLog(level, message, optionalParams) {
63
+ if (!this.#shouldLog(level)) {
64
+ return;
65
+ }
66
+ const { preparedMessage, invalidMessageInfo, messageError, messageExtra, } = this.#prepareMessage(message);
67
+ const { error, extra } = this.#extractErrorAndExtra(optionalParams, level);
68
+ const finalError = messageError || error;
69
+ const finalExtra = {
70
+ ...messageExtra,
71
+ ...extra,
72
+ };
73
+ const logEntry = this.#createLogEntry(level, preparedMessage, finalExtra, finalError, invalidMessageInfo);
74
+ const sanitizedLogEntry = (0, sanitize_js_1.sanitizeLogEntry)(logEntry, {
75
+ maskFields: this.#maskFields,
76
+ maxArrayLength: this.#maxArrayLength,
77
+ });
78
+ const output = this.#isDevelopment
79
+ ? (0, format_js_1.formatColoredJson)(sanitizedLogEntry, level)
80
+ : (0, shared_1.safeStringify)(sanitizedLogEntry);
81
+ console.log(output);
82
+ }
83
+ #prepareMessage(message) {
84
+ if (typeof message === 'string') {
85
+ return { preparedMessage: message };
86
+ }
87
+ if (message instanceof Error) {
88
+ return {
89
+ preparedMessage: message.message,
90
+ messageError: message,
91
+ };
92
+ }
93
+ if ((0, shared_1.isPlainObject)(message)) {
94
+ const foundError = (0, sanitize_js_1.findNestedError)(message);
95
+ if (foundError) {
96
+ return {
97
+ preparedMessage: foundError.message,
98
+ messageError: foundError,
99
+ messageExtra: message,
100
+ };
101
+ }
102
+ return {
103
+ preparedMessage: 'Object logged',
104
+ messageExtra: message,
105
+ };
106
+ }
107
+ const stack = new Error().stack
108
+ ?.split('\n')
109
+ .slice(2, 7)
110
+ .join('\n');
111
+ const preparedMessage = message === null || message === undefined
112
+ ? `[${String(message)}]`
113
+ : `[OBJECT]: ${(0, shared_1.safeStringify)(message)}`;
114
+ const invalidMessageInfo = {
115
+ invalidMessageWarning: 'Logger called with non-string message parameter',
116
+ invalidMessageCallstack: stack,
117
+ originalMessageType: typeof message,
118
+ originalMessage: (0, shared_1.safeStringify)(message),
119
+ };
120
+ return {
121
+ preparedMessage,
122
+ invalidMessageInfo,
123
+ };
124
+ }
125
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: handles multiple error extraction patterns
126
+ #extractErrorAndExtra(params, level) {
127
+ let error = null;
128
+ const extra = {};
129
+ for (const param of params) {
130
+ if (param instanceof Error) {
131
+ error = param;
132
+ }
133
+ else if (typeof param === 'string') {
134
+ const isErrorLevel = level === types_js_1.LogLevel.WARN ||
135
+ level === types_js_1.LogLevel.ERROR ||
136
+ level === types_js_1.LogLevel.FATAL;
137
+ if (isErrorLevel) {
138
+ error = new Error(param);
139
+ }
140
+ else {
141
+ extra.context = param;
142
+ }
143
+ }
144
+ else if ((0, shared_1.isPlainObject)(param)) {
145
+ const isErrorLevel = level === types_js_1.LogLevel.WARN ||
146
+ level === types_js_1.LogLevel.ERROR ||
147
+ level === types_js_1.LogLevel.FATAL;
148
+ if (param.err instanceof Error) {
149
+ error = param.err;
150
+ const { err: _, ...rest } = param;
151
+ Object.assign(extra, rest);
152
+ }
153
+ else if (param.error instanceof Error) {
154
+ error = param.error;
155
+ const { error: _, ...rest } = param;
156
+ Object.assign(extra, rest);
157
+ }
158
+ else if (isErrorLevel &&
159
+ typeof param.err === 'string') {
160
+ error = new Error(param.err);
161
+ const { err: _, ...rest } = param;
162
+ Object.assign(extra, rest);
163
+ }
164
+ else if (isErrorLevel &&
165
+ typeof param.error === 'string') {
166
+ error = new Error(param.error);
167
+ const { error: _, ...rest } = param;
168
+ Object.assign(extra, rest);
169
+ }
170
+ else {
171
+ const foundError = (0, sanitize_js_1.findNestedError)(param);
172
+ if (foundError) {
173
+ error = foundError;
174
+ }
175
+ Object.assign(extra, param);
176
+ }
177
+ }
178
+ }
179
+ return { error, extra };
180
+ }
181
+ #createLogEntry(level, message, extra, error, invalidMessageInfo) {
182
+ const ctx = this.#context
183
+ ? this.#context.getContext()
184
+ : {};
185
+ const logEntry = {
186
+ level,
187
+ timestamp: new Date().toISOString(),
188
+ pid: process.pid,
189
+ message,
190
+ ...(this.appId ? { appId: this.appId } : {}),
191
+ ...ctx,
192
+ ...extra,
193
+ ...(invalidMessageInfo || {}),
194
+ };
195
+ if (error) {
196
+ logEntry.error = {
197
+ name: error.name,
198
+ message: error.message,
199
+ stack: error.stack?.replace(/\n(\s+)?/g, ','),
200
+ };
201
+ }
202
+ return logEntry;
203
+ }
204
+ #shouldLog(level) {
205
+ const configuredIdx = types_js_1.LOG_LEVELS.indexOf(this.logLevel);
206
+ const messageIdx = types_js_1.LOG_LEVELS.indexOf(level);
207
+ if (messageIdx < configuredIdx) {
208
+ return false;
209
+ }
210
+ if (this.#context) {
211
+ const ctx = this.#context.getContext();
212
+ if (ctx.event &&
213
+ this.#filterEvents.includes(ctx.event)) {
214
+ return false;
215
+ }
216
+ }
217
+ return true;
218
+ }
219
+ }
220
+ exports.Logger = Logger;
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeLogEntry = sanitizeLogEntry;
4
+ exports.findNestedError = findNestedError;
5
+ const shared_1 = require("@arkv/shared");
6
+ function sanitizeLogEntry(obj, options, visited = new WeakSet()) {
7
+ if (visited.has(obj)) {
8
+ return {
9
+ '[Circular]': 'circular reference detected',
10
+ };
11
+ }
12
+ visited.add(obj);
13
+ const cleaned = {};
14
+ for (const [key, value] of Object.entries(obj)) {
15
+ if (value === undefined || value === null) {
16
+ continue;
17
+ }
18
+ const shouldMask = options.maskFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
19
+ if (shouldMask) {
20
+ cleaned[key] = '[MASKED]';
21
+ }
22
+ else {
23
+ const safeValue = makeSafeForJson(value, options);
24
+ if (safeValue !== undefined) {
25
+ if (Array.isArray(safeValue)) {
26
+ cleaned[key] = sanitizeArray(safeValue, options, visited);
27
+ }
28
+ else if ((0, shared_1.isPlainObject)(safeValue)) {
29
+ cleaned[key] = sanitizeLogEntry(safeValue, options, visited);
30
+ }
31
+ else {
32
+ cleaned[key] = safeValue;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return cleaned;
38
+ }
39
+ function sanitizeArray(array, options, visited) {
40
+ return array.map(item => {
41
+ if ((0, shared_1.isPlainObject)(item)) {
42
+ return sanitizeLogEntry(item, options, visited);
43
+ }
44
+ if (Array.isArray(item)) {
45
+ return sanitizeArray(item, options, visited);
46
+ }
47
+ return makeSafeForJson(item, options);
48
+ });
49
+ }
50
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive error search
51
+ function findNestedError(obj, visited = new WeakSet()) {
52
+ if (!obj || typeof obj !== 'object') {
53
+ return null;
54
+ }
55
+ if (visited.has(obj)) {
56
+ return null;
57
+ }
58
+ visited.add(obj);
59
+ for (const value of Object.values(obj)) {
60
+ if (value instanceof Error) {
61
+ return value;
62
+ }
63
+ if ((0, shared_1.isPlainObject)(value)) {
64
+ const nestedError = findNestedError(value, visited);
65
+ if (nestedError) {
66
+ return nestedError;
67
+ }
68
+ }
69
+ if (Array.isArray(value)) {
70
+ for (const item of value) {
71
+ if (item instanceof Error) {
72
+ return item;
73
+ }
74
+ if ((0, shared_1.isPlainObject)(item)) {
75
+ const nestedError = findNestedError(item, visited);
76
+ if (nestedError) {
77
+ return nestedError;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: handles many type serialization cases
86
+ function makeSafeForJson(value, options) {
87
+ if (value === null || value === undefined) {
88
+ return value;
89
+ }
90
+ const valueType = typeof value;
91
+ if (valueType === 'function') {
92
+ return `[Function: ${value.name || 'anonymous'}]`;
93
+ }
94
+ if (valueType === 'symbol') {
95
+ return `[Symbol: ${value.toString()}]`;
96
+ }
97
+ if (valueType === 'bigint') {
98
+ return `[BigInt: ${value.toString()}]`;
99
+ }
100
+ if (valueType !== 'object') {
101
+ return value;
102
+ }
103
+ if (value instanceof Date) {
104
+ return value.toISOString();
105
+ }
106
+ if (value instanceof RegExp) {
107
+ return `[RegExp: ${value.toString()}]`;
108
+ }
109
+ if (value instanceof Error) {
110
+ return {
111
+ name: value.name,
112
+ message: value.message,
113
+ stack: value.stack?.replace(/\n(\s+)?/g, ','),
114
+ };
115
+ }
116
+ if (typeof FormData !== 'undefined' &&
117
+ value instanceof FormData) {
118
+ const entries = {};
119
+ try {
120
+ for (const [key, val] of value.entries()) {
121
+ if (val &&
122
+ typeof val === 'object' &&
123
+ 'name' in val &&
124
+ 'size' in val &&
125
+ 'type' in val) {
126
+ const file = val;
127
+ entries[key] =
128
+ `[File: ${file.name} (${file.size} bytes, ${file.type})]`;
129
+ }
130
+ else {
131
+ entries[key] = val;
132
+ }
133
+ }
134
+ return { '[FormData]': entries };
135
+ }
136
+ catch {
137
+ return '[FormData: unable to read entries]';
138
+ }
139
+ }
140
+ if (value &&
141
+ typeof value === 'object' &&
142
+ 'name' in value &&
143
+ 'size' in value &&
144
+ 'type' in value &&
145
+ typeof value
146
+ .arrayBuffer === 'function') {
147
+ const file = value;
148
+ return `[File: ${file.name} (${file.size} bytes, ${file.type})]`;
149
+ }
150
+ if (typeof Blob !== 'undefined' &&
151
+ value instanceof Blob) {
152
+ return `[Blob: ${value.size} bytes, ${value.type}]`;
153
+ }
154
+ if (typeof ArrayBuffer !== 'undefined' &&
155
+ value instanceof ArrayBuffer) {
156
+ return `[ArrayBuffer: ${value.byteLength} bytes]`;
157
+ }
158
+ if (Array.isArray(value)) {
159
+ return sliceArray(value, options);
160
+ }
161
+ if ((0, shared_1.isPlainObject)(value)) {
162
+ return value;
163
+ }
164
+ try {
165
+ JSON.stringify(value);
166
+ return value;
167
+ }
168
+ catch {
169
+ if (value
170
+ .constructor?.name) {
171
+ return `[${value.constructor.name}: object not serializable]`;
172
+ }
173
+ return '[Object: not serializable]';
174
+ }
175
+ }
176
+ function sliceArray(array, options) {
177
+ if (array.length <= options.maxArrayLength) {
178
+ return array.map(item => makeSafeForJson(item, options));
179
+ }
180
+ const slicedArray = array
181
+ .slice(0, options.maxArrayLength)
182
+ .map(item => makeSafeForJson(item, options));
183
+ return [
184
+ ...slicedArray,
185
+ `[TRUNCATED: ${array.length - options.maxArrayLength} more items]`,
186
+ ];
187
+ }