@lakutata/logger 0.0.0-stage → 3.0.0-beta.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.
Files changed (41) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +137 -2
  3. package/THIRD_PARTY_NOTICES.md +158 -0
  4. package/dist/cjs/Logger.d.ts +291 -0
  5. package/dist/cjs/Logger.js +185 -0
  6. package/dist/cjs/exports/Logger.d.ts +2 -0
  7. package/dist/cjs/exports/Logger.js +20 -0
  8. package/dist/cjs/lib/Colors.d.ts +8 -0
  9. package/dist/cjs/lib/Colors.js +48 -0
  10. package/dist/cjs/lib/Destination.d.ts +38 -0
  11. package/dist/cjs/lib/Destination.js +103 -0
  12. package/dist/cjs/lib/Format.d.ts +7 -0
  13. package/dist/cjs/lib/Format.js +106 -0
  14. package/dist/cjs/lib/Pretty.d.ts +9 -0
  15. package/dist/cjs/lib/Pretty.js +265 -0
  16. package/dist/cjs/lib/Record.d.ts +25 -0
  17. package/dist/cjs/lib/Record.js +139 -0
  18. package/dist/cjs/lib/Serializers.d.ts +26 -0
  19. package/dist/cjs/lib/Serializers.js +150 -0
  20. package/dist/cjs/lib/Stringify.d.ts +5 -0
  21. package/dist/cjs/lib/Stringify.js +110 -0
  22. package/dist/cjs/package.json +1 -0
  23. package/dist/esm/Logger.js +182 -0
  24. package/dist/esm/exports/Logger.js +1 -0
  25. package/dist/esm/lib/Colors.js +45 -0
  26. package/dist/esm/lib/Destination.js +99 -0
  27. package/dist/esm/lib/Format.js +103 -0
  28. package/dist/esm/lib/Pretty.js +262 -0
  29. package/dist/esm/lib/Record.js +134 -0
  30. package/dist/esm/lib/Serializers.js +143 -0
  31. package/dist/esm/lib/Stringify.js +107 -0
  32. package/dist/types/Logger.d.ts +291 -0
  33. package/dist/types/exports/Logger.d.ts +2 -0
  34. package/dist/types/lib/Colors.d.ts +8 -0
  35. package/dist/types/lib/Destination.d.ts +38 -0
  36. package/dist/types/lib/Format.d.ts +7 -0
  37. package/dist/types/lib/Pretty.d.ts +9 -0
  38. package/dist/types/lib/Record.d.ts +25 -0
  39. package/dist/types/lib/Serializers.d.ts +26 -0
  40. package/dist/types/lib/Stringify.d.ts +5 -0
  41. package/package.json +43 -4
@@ -0,0 +1,262 @@
1
+ const EOL = '\n';
2
+ const IDENT = ' ';
3
+ const LEVEL_LABELS = { default: 'USERLVL', 60: 'FATAL', 50: 'ERROR', 40: 'WARN', 30: 'INFO', 20: 'DEBUG', 10: 'TRACE' };
4
+ const LEVEL_NUMBERS = { fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 };
5
+ //The keys of the logger, not printed as properties
6
+ const LOGGER_KEYS = ['pid', 'hostname', 'name', 'level', 'time', 'timestamp', 'caller'];
7
+ const ERROR_KEYS = new Set(['err', 'error']);
8
+ /**
9
+ * The JSON of a property value, indented by 2 spaces
10
+ * @param value
11
+ */
12
+ function propertyJSON(value) {
13
+ switch (typeof value) {
14
+ case 'number':
15
+ case 'boolean':
16
+ return String(value);
17
+ case 'string':
18
+ return JSON.stringify(value);
19
+ default:
20
+ return JSON.stringify(value, null, 2);
21
+ }
22
+ }
23
+ const hasOwn = Object.hasOwn;
24
+ //The prototype pollution suspects of a JSON text
25
+ const SUSPECT_PROTO = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
26
+ const SUSPECT_CONSTRUCTOR = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
27
+ /**
28
+ * Remove the "__proto__" properties of a parsed log, false if it has a constructor with a prototype (not printed as a log)
29
+ * @param log
30
+ */
31
+ function sanitize(log) {
32
+ let next = [log];
33
+ try {
34
+ while (next.length) {
35
+ const nodes = next;
36
+ next = [];
37
+ for (const node of nodes) {
38
+ if (hasOwn(node, '__proto__'))
39
+ delete node.__proto__;
40
+ if (hasOwn(node, 'constructor') && Object.prototype.hasOwnProperty.call(node.constructor, 'prototype'))
41
+ return false;
42
+ for (const key in node) {
43
+ const value = node[key];
44
+ if (value && typeof value === 'object')
45
+ next.push(value);
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+ /**
56
+ * Indent the lines after the first one
57
+ * @param input
58
+ * @param ident
59
+ */
60
+ function joinLines(input, ident = IDENT) {
61
+ const lines = input.split(/\r?\n/);
62
+ for (let index = 1; index < lines.length; index++)
63
+ lines[index] = ident + lines[index];
64
+ return lines.join(EOL);
65
+ }
66
+ /**
67
+ * Whether a value is a plain object
68
+ * @param value
69
+ */
70
+ function isObjectValue(value) {
71
+ return Object.prototype.toString.call(value) === '[object Object]';
72
+ }
73
+ /**
74
+ * Pad a number with zeros
75
+ * @param value
76
+ * @param length
77
+ */
78
+ function pad(value, length = 2) {
79
+ return String(value).padStart(length, '0');
80
+ }
81
+ /**
82
+ * The time of a log, HH:MM:ss.SSS in the local time zone
83
+ * @param log
84
+ */
85
+ function prettyTime(log) {
86
+ let time = null;
87
+ if ('time' in log) {
88
+ time = log.time;
89
+ }
90
+ else if ('timestamp' in log) {
91
+ time = log.timestamp;
92
+ }
93
+ if (time === null)
94
+ return undefined;
95
+ let date = new Date(time);
96
+ if (isNaN(date.getTime()))
97
+ date = new Date(+time);
98
+ if (isNaN(date.getTime()))
99
+ return `[${time}]`;
100
+ return `[${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}]`;
101
+ }
102
+ /**
103
+ * The label of the level of a log
104
+ * @param log
105
+ * @param palette
106
+ */
107
+ function prettyLevel(log, palette) {
108
+ if (!hasOwn(log, 'level'))
109
+ return undefined;
110
+ const level = log.level;
111
+ if (level === undefined)
112
+ return undefined;
113
+ let levelNumber = 'default';
114
+ if (Number.isInteger(+level)) {
115
+ levelNumber = hasOwn(LEVEL_LABELS, level) ? level : levelNumber;
116
+ }
117
+ else {
118
+ const name = String(level).toLowerCase();
119
+ levelNumber = hasOwn(LEVEL_NUMBERS, name) ? LEVEL_NUMBERS[name] : levelNumber;
120
+ }
121
+ const label = LEVEL_LABELS[levelNumber];
122
+ return hasOwn(palette.level, levelNumber) ? palette.level[levelNumber](label) : palette.level.default(label);
123
+ }
124
+ /**
125
+ * The metadata of a log: (name/pid on hostname) <caller>
126
+ * @param log
127
+ */
128
+ function prettyMetadata(log) {
129
+ let line = '';
130
+ if (log.name || log.pid || log.hostname) {
131
+ line += '(';
132
+ if (log.name)
133
+ line += log.name;
134
+ if (log.pid)
135
+ line += log.name ? `/${log.pid}` : log.pid;
136
+ if (log.hostname)
137
+ line += `${line === '(' ? 'on' : ' on'} ${log.hostname}`;
138
+ line += ')';
139
+ }
140
+ if (log.caller)
141
+ line += `${line === '' ? '' : ' '}<${log.caller}>`;
142
+ return line === '' ? undefined : line;
143
+ }
144
+ /**
145
+ * An error property, its stack is written on its own lines
146
+ * @param key
147
+ * @param lines
148
+ */
149
+ function prettyError(key, lines) {
150
+ let result = '';
151
+ const splitLines = `${IDENT}${key}: ${joinLines(lines)}${EOL}`.split(EOL);
152
+ for (let index = 0; index < splitLines.length; index++) {
153
+ if (index)
154
+ result += EOL;
155
+ const line = splitLines[index];
156
+ const matches = /^\s*"stack"/.test(line) ? /^(\s*"stack":)\s*(".*"),?$/.exec(line) : null;
157
+ if (matches) {
158
+ const indentation = ' '.repeat(/^\s*/.exec(line)[0].length + 4);
159
+ result += matches[1] + EOL + indentation + JSON.parse(matches[2]).replace(/\n/g, EOL + indentation);
160
+ }
161
+ else {
162
+ result += line;
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+ /**
168
+ * The properties of a log, one per line
169
+ * @param log
170
+ * @param palette
171
+ * @param skipKeys
172
+ * @param excludeLoggerKeys
173
+ * @param ident
174
+ */
175
+ function prettyObject(log, palette, skipKeys, excludeLoggerKeys = true, ident = IDENT) {
176
+ let result = '';
177
+ let errors;
178
+ for (const key in log) {
179
+ if (!hasOwn(log, key) || skipKeys.includes(key) || (excludeLoggerKeys && LOGGER_KEYS.includes(key)))
180
+ continue;
181
+ const value = log[key];
182
+ if (ERROR_KEYS.has(key)) {
183
+ (errors ??= []).push([key, value]);
184
+ continue;
185
+ }
186
+ let lines = propertyJSON(value);
187
+ if (lines === undefined)
188
+ continue;
189
+ if (lines.includes('\\\\'))
190
+ lines = lines.replace(/\\\\/gi, '\\');
191
+ const joinedLines = lines.includes('\n') ? joinLines(lines, ident) : lines;
192
+ result += `${ident}${palette.property(key)}:${joinedLines.startsWith(EOL) ? '' : ' '}${joinedLines}${EOL}`;
193
+ }
194
+ if (!errors)
195
+ return result;
196
+ for (const [key, value] of errors) {
197
+ const lines = JSON.stringify(value, null, 2);
198
+ if (lines !== undefined)
199
+ result += prettyError(key, lines);
200
+ }
201
+ return result;
202
+ }
203
+ /**
204
+ * A log which is an error: its stack
205
+ * @param log
206
+ * @param palette
207
+ */
208
+ function prettyErrorLog(log, palette) {
209
+ let result = `${IDENT}${joinLines(log.stack)}${EOL}`;
210
+ //The error properties to print: none
211
+ if ('' in log) {
212
+ result += isObjectValue(log['']) ? `${IDENT}: {${EOL}${prettyObject(log[''], palette, [], false, IDENT + IDENT)}${IDENT}}${EOL}` : `${IDENT}: ${log['']}${EOL}`;
213
+ }
214
+ return result;
215
+ }
216
+ /**
217
+ * Format a JSON log line for humans:
218
+ * [time] LEVEL (name/pid): message, then the properties on their own lines
219
+ * @param line the JSON log line, without its line terminator
220
+ * @param palette
221
+ * @param parsed the parsed line, if it is known (the log is changed)
222
+ */
223
+ export function pretty(line, palette, parsed) {
224
+ let log;
225
+ if (parsed) {
226
+ log = parsed;
227
+ }
228
+ else {
229
+ try {
230
+ log = JSON.parse(line);
231
+ }
232
+ catch {
233
+ return line + EOL;
234
+ }
235
+ if (!isObjectValue(log))
236
+ return line + EOL;
237
+ }
238
+ if ((SUSPECT_PROTO.test(line) || SUSPECT_CONSTRUCTOR.test(line)) && !sanitize(log))
239
+ return line + EOL;
240
+ const message = 'msg' in log && (typeof log.msg === 'string' || typeof log.msg === 'number' || typeof log.msg === 'boolean') ? palette.message(log.msg) : undefined;
241
+ //The host name is not printed
242
+ if (hasOwn(log, 'hostname'))
243
+ delete log.hostname;
244
+ const level = prettyLevel(log, palette);
245
+ const metadata = prettyMetadata(log);
246
+ const time = prettyTime(log);
247
+ let result = time ?? '';
248
+ if (level)
249
+ result = result ? `${result} ${level}` : level;
250
+ if (metadata)
251
+ result = result ? `${result} ${metadata}:` : metadata;
252
+ if (!result.endsWith(':') && result !== '')
253
+ result += ':';
254
+ if (message !== undefined)
255
+ result = result ? `${result} ${message}` : message;
256
+ if (result.length)
257
+ result += EOL;
258
+ if (log.type === 'Error' && typeof log.stack === 'string')
259
+ return result + prettyErrorLog(log, palette);
260
+ const skipKeys = ['msg', 'level', 'time'].filter((key) => typeof log[key] === 'string' || typeof log[key] === 'number' || typeof log[key] === 'boolean');
261
+ return result + prettyObject(log, palette, skipKeys);
262
+ }
@@ -0,0 +1,134 @@
1
+ //Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
2
+ import { hostname } from 'node:os';
3
+ import { format } from './Format.js';
4
+ import { serializeError, serializeRequest, serializeResponse } from './Serializers.js';
5
+ import { stringify } from './Stringify.js';
6
+ export const LEVELS = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, silent: Infinity };
7
+ const HOSTNAME = hostname();
8
+ /**
9
+ * The JSON of a log value, undefined if it is not logged
10
+ * @param value
11
+ */
12
+ function valueJSON(value) {
13
+ switch (typeof value) {
14
+ case 'undefined':
15
+ case 'function':
16
+ return undefined;
17
+ case 'number':
18
+ return Number.isFinite(value) ? String(value) : 'null';
19
+ case 'boolean':
20
+ return String(value);
21
+ case 'string':
22
+ return JSON.stringify(value);
23
+ default:
24
+ return stringify(value);
25
+ }
26
+ }
27
+ /**
28
+ * The bindings of the log lines of an application: pid, hostname and name
29
+ * @param name
30
+ */
31
+ export function logBindings(name) {
32
+ return { json: `,"pid":${process.pid},"hostname":${JSON.stringify(HOSTNAME)},"name":${JSON.stringify(name)}`, name: name };
33
+ }
34
+ /**
35
+ * The value of a JSON text, as parsed
36
+ * @param value
37
+ * @param json
38
+ */
39
+ function parsedValue(value, json) {
40
+ switch (typeof value) {
41
+ case 'string':
42
+ case 'boolean':
43
+ return value;
44
+ case 'number':
45
+ return Number.isFinite(value) ? value + 0 : null;
46
+ default:
47
+ return JSON.parse(json);
48
+ }
49
+ }
50
+ /**
51
+ * Set a property of a parsed log, as JSON.parse does (the first position and the last value of a key)
52
+ * @param log
53
+ * @param key
54
+ * @param value
55
+ */
56
+ function setParsed(log, key, value) {
57
+ key === '__proto__' ? Object.defineProperty(log, key, { value: value, enumerable: true, writable: true, configurable: true }) : log[key] = value;
58
+ }
59
+ /**
60
+ * Build a JSON log line from the arguments of a log method:
61
+ * an object (or error, HTTP request or response) with an optional message and its format arguments,
62
+ * or a message with its format arguments
63
+ * @param level
64
+ * @param bindings
65
+ * @param args
66
+ * @param parse whether to also build the parsed line
67
+ * @param redacted the lower-case names of the headers of an HTTP request or response written as [Redacted]
68
+ */
69
+ export function buildRecord(level, bindings, args, parse = false, redacted = new Set()) {
70
+ let object = null;
71
+ let message;
72
+ let [first, ...rest] = args;
73
+ if (typeof first === 'object') {
74
+ message = first;
75
+ if (first !== null) {
76
+ if (first.method && first.headers && first.socket) {
77
+ first = { req: serializeRequest(first, redacted) };
78
+ }
79
+ else if (typeof first.setHeader === 'function') {
80
+ first = { res: serializeResponse(first, redacted) };
81
+ }
82
+ }
83
+ let formatArgs;
84
+ if (message === null && !rest.length) {
85
+ formatArgs = [null];
86
+ }
87
+ else {
88
+ message = rest.shift();
89
+ formatArgs = rest;
90
+ }
91
+ object = first;
92
+ message = format(message, formatArgs);
93
+ }
94
+ else {
95
+ message = format(first === undefined ? rest.shift() : first, rest);
96
+ }
97
+ if (object === undefined || object === null) {
98
+ object = {};
99
+ }
100
+ else if (object instanceof Error) {
101
+ if (message === undefined)
102
+ message = object.message;
103
+ object = { err: object };
104
+ }
105
+ else if (message === undefined && object.msg === undefined && object.err) {
106
+ message = object.err.message;
107
+ }
108
+ const time = Date.now();
109
+ let line = `{"level":${level},"time":${time}${bindings.json}`;
110
+ const log = parse ? { level: level, time: time, pid: process.pid, hostname: HOSTNAME, name: bindings.name } : undefined;
111
+ for (const key in object) {
112
+ let value = object[key];
113
+ if (!Object.prototype.hasOwnProperty.call(object, key) || value === undefined)
114
+ continue;
115
+ if (key === 'err')
116
+ value = serializeError(value);
117
+ const json = valueJSON(value);
118
+ if (json === undefined)
119
+ continue;
120
+ line += `,${JSON.stringify(key)}:${json}`;
121
+ if (log)
122
+ setParsed(log, key, parsedValue(value, json));
123
+ }
124
+ if (message !== undefined && typeof message !== 'function') {
125
+ const json = typeof message === 'string' ? JSON.stringify(message) : valueJSON(message);
126
+ //A message which cannot be serialized (a symbol) is omitted
127
+ if (json !== undefined) {
128
+ line += `,"msg":${json}`;
129
+ if (log)
130
+ setParsed(log, 'msg', parsedValue(message, json));
131
+ }
132
+ }
133
+ return { line: `${line}}\n`, log: log };
134
+ }
@@ -0,0 +1,143 @@
1
+ //Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
2
+ const SEEN = Symbol('seen');
3
+ /**
4
+ * Whether a value looks like an error
5
+ * @param value
6
+ */
7
+ export function isErrorLike(value) {
8
+ return !!value && typeof value.message === 'string';
9
+ }
10
+ /**
11
+ * The cause of an error
12
+ * @param error
13
+ */
14
+ function causeOf(error) {
15
+ if (!error)
16
+ return undefined;
17
+ const cause = typeof error.cause === 'function' ? error.cause() : error.cause;
18
+ return isErrorLike(cause) ? cause : undefined;
19
+ }
20
+ /**
21
+ * The stack of an error followed by the stacks of its causes
22
+ * @param error
23
+ * @param seen
24
+ */
25
+ function stackWithCauses(error, seen = new Set()) {
26
+ if (!isErrorLike(error))
27
+ return '';
28
+ const stack = error.stack || '';
29
+ if (seen.has(error))
30
+ return `${stack}\ncauses have become circular...`;
31
+ const cause = causeOf(error);
32
+ if (!cause)
33
+ return stack;
34
+ seen.add(error);
35
+ return `${stack}\ncaused by: ${stackWithCauses(cause, seen)}`;
36
+ }
37
+ /**
38
+ * The message of an error followed by the messages of its causes
39
+ * @param error
40
+ * @param seen
41
+ * @param skip
42
+ */
43
+ function messageWithCauses(error, seen = new Set(), skip = false) {
44
+ if (!isErrorLike(error))
45
+ return '';
46
+ const message = skip ? '' : (error.message || '');
47
+ if (seen.has(error))
48
+ return `${message}: ...`;
49
+ const cause = causeOf(error);
50
+ if (!cause)
51
+ return message;
52
+ seen.add(error);
53
+ //A cause function (VError) already includes the message of its cause, as an error wrapping a cause of the same
54
+ //message (an exception keeping the original error)
55
+ const included = typeof error.cause === 'function' || (!skip && !seen.has(cause) && message === cause.message);
56
+ return message + (included ? '' : ': ') + messageWithCauses(cause, seen, included);
57
+ }
58
+ /**
59
+ * Serialize an error: type, message and stack (with the causes), aggregated errors and the other enumerable properties
60
+ * @param error
61
+ */
62
+ export function serializeError(error) {
63
+ if (!isErrorLike(error))
64
+ return error;
65
+ error[SEEN] = undefined;
66
+ const serialized = {};
67
+ serialized.type = Object.prototype.toString.call(error.constructor) === '[object Function]' ? error.constructor.name : error.name;
68
+ serialized.message = messageWithCauses(error);
69
+ serialized.stack = stackWithCauses(error);
70
+ if (Array.isArray(error.errors))
71
+ serialized.aggregateErrors = error.errors.map((item) => serializeError(item));
72
+ for (const key in error) {
73
+ //"raw" is reserved for the original error (not serialized)
74
+ if (serialized[key] !== undefined || key === 'raw')
75
+ continue;
76
+ const value = error[key];
77
+ if (isErrorLike(value)) {
78
+ if (key !== 'cause' && !Object.prototype.hasOwnProperty.call(value, SEEN))
79
+ serialized[key] = serializeError(value);
80
+ }
81
+ else {
82
+ serialized[key] = value;
83
+ }
84
+ }
85
+ delete error[SEEN];
86
+ return serialized;
87
+ }
88
+ /**
89
+ * The headers written as [Redacted] by default: the credentials of the requests and of the responses
90
+ */
91
+ export const REDACTED_HEADERS = Object.freeze(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
92
+ /**
93
+ * A copy of headers, the values of the redacted ones (lower-case names) replaced by [Redacted]
94
+ * @param headers
95
+ * @param redacted
96
+ */
97
+ function redactHeaders(headers, redacted) {
98
+ if (!redacted.size || !headers || typeof headers !== 'object')
99
+ return headers;
100
+ //A copy: the headers of the request or the response are not changed
101
+ const copy = { ...headers };
102
+ for (const name of Object.keys(copy))
103
+ if (redacted.has(name.toLowerCase()))
104
+ copy[name] = '[Redacted]';
105
+ return copy;
106
+ }
107
+ /**
108
+ * Serialize an HTTP request
109
+ * @param request
110
+ * @param redacted the lower-case names of the headers written as [Redacted]
111
+ */
112
+ export function serializeRequest(request, redacted = new Set()) {
113
+ const connection = request.info || request.socket;
114
+ const serialized = {};
115
+ serialized.id = typeof request.id === 'function' ? request.id() : (request.id || (request.info ? request.info.id : undefined));
116
+ serialized.method = request.method;
117
+ if (request.originalUrl) {
118
+ serialized.url = request.originalUrl;
119
+ }
120
+ else {
121
+ const path = request.path;
122
+ serialized.url = typeof path === 'string' ? path : (request.url ? request.url.path || request.url : undefined);
123
+ }
124
+ if (request.query)
125
+ serialized.query = request.query;
126
+ if (request.params)
127
+ serialized.params = request.params;
128
+ serialized.headers = redactHeaders(request.headers, redacted);
129
+ serialized.remoteAddress = connection && connection.remoteAddress;
130
+ serialized.remotePort = connection && connection.remotePort;
131
+ return serialized;
132
+ }
133
+ /**
134
+ * Serialize an HTTP response
135
+ * @param response
136
+ * @param redacted the lower-case names of the headers written as [Redacted]
137
+ */
138
+ export function serializeResponse(response, redacted = new Set()) {
139
+ return {
140
+ statusCode: response.headersSent ? response.statusCode : null,
141
+ headers: redactHeaders(response.getHeaders ? response.getHeaders() : response._headers, redacted)
142
+ };
143
+ }
@@ -0,0 +1,107 @@
1
+ //Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
2
+ const MAXIMUM_DEPTH = 5;
3
+ const MAXIMUM_BREADTH = 100;
4
+ const TypedArrayToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Int8Array.prototype), Symbol.toStringTag).get;
5
+ /**
6
+ * The count of the omitted items
7
+ * @param count
8
+ */
9
+ function itemCount(count) {
10
+ return count === 1 ? '1 item' : `${count} items`;
11
+ }
12
+ /**
13
+ * Serialize a value which JSON cannot (circular references, bigints):
14
+ * sorted keys, circular references as "[Circular]", limited depth and breadth
15
+ * @param key
16
+ * @param value
17
+ * @param stack
18
+ */
19
+ function safeStringify(key, value, stack) {
20
+ switch (typeof value) {
21
+ case 'string':
22
+ return JSON.stringify(value);
23
+ case 'number':
24
+ return isFinite(value) ? String(value) : 'null';
25
+ case 'boolean':
26
+ return value ? 'true' : 'false';
27
+ case 'bigint':
28
+ return String(value);
29
+ case 'object': {
30
+ if (value === null)
31
+ return 'null';
32
+ if (typeof value.toJSON === 'function') {
33
+ value = value.toJSON(key);
34
+ if (typeof value !== 'object')
35
+ return safeStringify(key, value, stack);
36
+ if (value === null)
37
+ return 'null';
38
+ }
39
+ if (stack.includes(value))
40
+ return '"[Circular]"';
41
+ let result = '';
42
+ if (Array.isArray(value)) {
43
+ if (!value.length)
44
+ return '[]';
45
+ if (MAXIMUM_DEPTH < stack.length + 1)
46
+ return '"[Array]"';
47
+ stack.push(value);
48
+ const count = Math.min(value.length, MAXIMUM_BREADTH);
49
+ for (let index = 0; index < count; index++) {
50
+ result += `${index ? ',' : ''}${safeStringify(String(index), value[index], stack) ?? 'null'}`;
51
+ }
52
+ if (value.length - 1 > MAXIMUM_BREADTH)
53
+ result += `,"... ${itemCount(value.length - MAXIMUM_BREADTH - 1)} not stringified"`;
54
+ stack.pop();
55
+ return `[${result}]`;
56
+ }
57
+ let keys = Object.keys(value);
58
+ const keyCount = keys.length;
59
+ if (!keyCount)
60
+ return '{}';
61
+ if (MAXIMUM_DEPTH < stack.length + 1)
62
+ return '"[Object]"';
63
+ let separator = '';
64
+ let count = Math.min(keyCount, MAXIMUM_BREADTH);
65
+ if (value.length !== undefined && TypedArrayToStringTag.call(value) !== undefined && value.length !== 0) {
66
+ const items = Math.min(value.length, MAXIMUM_BREADTH);
67
+ for (let index = 0; index < items; index++)
68
+ result += `${index ? ',' : ''}"${index}":${value[index]}`;
69
+ keys = keys.slice(value.length);
70
+ count -= value.length;
71
+ separator = ',';
72
+ }
73
+ keys.sort();
74
+ stack.push(value);
75
+ for (let index = 0; index < count; index++) {
76
+ const item = safeStringify(keys[index], value[keys[index]], stack);
77
+ if (item === undefined)
78
+ continue;
79
+ result += `${separator}${JSON.stringify(keys[index])}:${item}`;
80
+ separator = ',';
81
+ }
82
+ if (keyCount > MAXIMUM_BREADTH)
83
+ result += `${separator}"...":"${itemCount(keyCount - MAXIMUM_BREADTH)} not stringified"`;
84
+ stack.pop();
85
+ return `{${result}}`;
86
+ }
87
+ default:
88
+ return undefined;
89
+ }
90
+ }
91
+ /**
92
+ * Serialize a value to JSON, the values JSON cannot serialize are serialized safely
93
+ * @param value
94
+ */
95
+ export function stringify(value) {
96
+ try {
97
+ return JSON.stringify(value);
98
+ }
99
+ catch {
100
+ try {
101
+ return safeStringify('', value, []);
102
+ }
103
+ catch {
104
+ return '"[unable to serialize, circular reference is too complex to analyze]"';
105
+ }
106
+ }
107
+ }