@logtape/logtape 1.4.0-dev.409 → 1.4.0-dev.413

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/src/logger.ts DELETED
@@ -1,1805 +0,0 @@
1
- import {
2
- type ContextLocalStorage,
3
- getCategoryPrefix,
4
- getImplicitContext,
5
- } from "./context.ts";
6
- import type { Filter } from "./filter.ts";
7
- import { compareLogLevel, type LogLevel } from "./level.ts";
8
- import type { LogRecord } from "./record.ts";
9
- import type { Sink } from "./sink.ts";
10
-
11
- /**
12
- * A logger interface. It provides methods to log messages at different
13
- * severity levels.
14
- *
15
- * ```typescript
16
- * const logger = getLogger("category");
17
- * logger.trace `A trace message with ${value}`
18
- * logger.debug `A debug message with ${value}.`;
19
- * logger.info `An info message with ${value}.`;
20
- * logger.warn `A warning message with ${value}.`;
21
- * logger.error `An error message with ${value}.`;
22
- * logger.fatal `A fatal error message with ${value}.`;
23
- * ```
24
- */
25
- export interface Logger {
26
- /**
27
- * The category of the logger. It is an array of strings.
28
- */
29
- readonly category: readonly string[];
30
-
31
- /**
32
- * The logger with the supercategory of the current logger. If the current
33
- * logger is the root logger, this is `null`.
34
- */
35
- readonly parent: Logger | null;
36
-
37
- /**
38
- * Get a child logger with the given subcategory.
39
- *
40
- * ```typescript
41
- * const logger = getLogger("category");
42
- * const subLogger = logger.getChild("sub-category");
43
- * ```
44
- *
45
- * The above code is equivalent to:
46
- *
47
- * ```typescript
48
- * const logger = getLogger("category");
49
- * const subLogger = getLogger(["category", "sub-category"]);
50
- * ```
51
- *
52
- * @param subcategory The subcategory.
53
- * @returns The child logger.
54
- */
55
- getChild(
56
- subcategory: string | readonly [string] | readonly [string, ...string[]],
57
- ): Logger;
58
-
59
- /**
60
- * Get a logger with contextual properties. This is useful for
61
- * log multiple messages with the shared set of properties.
62
- *
63
- * ```typescript
64
- * const logger = getLogger("category");
65
- * const ctx = logger.with({ foo: 123, bar: "abc" });
66
- * ctx.info("A message with {foo} and {bar}.");
67
- * ctx.warn("Another message with {foo}, {bar}, and {baz}.", { baz: true });
68
- * ```
69
- *
70
- * The above code is equivalent to:
71
- *
72
- * ```typescript
73
- * const logger = getLogger("category");
74
- * logger.info("A message with {foo} and {bar}.", { foo: 123, bar: "abc" });
75
- * logger.warn(
76
- * "Another message with {foo}, {bar}, and {baz}.",
77
- * { foo: 123, bar: "abc", baz: true },
78
- * );
79
- * ```
80
- *
81
- * @param properties
82
- * @returns
83
- * @since 0.5.0
84
- */
85
- with(properties: Record<string, unknown>): Logger;
86
-
87
- /**
88
- * Log a trace message. Use this as a template string prefix.
89
- *
90
- * ```typescript
91
- * logger.trace `A trace message with ${value}.`;
92
- * ```
93
- *
94
- * @param message The message template strings array.
95
- * @param values The message template values.
96
- * @since 0.12.0
97
- */
98
- trace(message: TemplateStringsArray, ...values: readonly unknown[]): void;
99
-
100
- /**
101
- * Log a trace message with properties.
102
- *
103
- * ```typescript
104
- * logger.trace('A trace message with {value}.', { value });
105
- * ```
106
- *
107
- * If the properties are expensive to compute, you can pass a callback that
108
- * returns the properties:
109
- *
110
- * ```typescript
111
- * logger.trace(
112
- * 'A trace message with {value}.',
113
- * () => ({ value: expensiveComputation() })
114
- * );
115
- * ```
116
- *
117
- * @param message The message template. Placeholders to be replaced with
118
- * `values` are indicated by keys in curly braces (e.g.,
119
- * `{value}`).
120
- * @param properties The values to replace placeholders with. For lazy
121
- * evaluation, this can be a callback that returns the
122
- * properties.
123
- * @since 0.12.0
124
- */
125
- trace(
126
- message: string,
127
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
128
- ): void;
129
-
130
- /**
131
- * Log a trace values with no message. This is useful when you
132
- * want to log properties without a message, e.g., when you want to log
133
- * the context of a request or an operation.
134
- *
135
- * ```typescript
136
- * logger.trace({ method: 'GET', url: '/api/v1/resource' });
137
- * ```
138
- *
139
- * Note that this is a shorthand for:
140
- *
141
- * ```typescript
142
- * logger.trace('{*}', { method: 'GET', url: '/api/v1/resource' });
143
- * ```
144
- *
145
- * If the properties are expensive to compute, you cannot use this shorthand
146
- * and should use the following syntax instead:
147
- *
148
- * ```typescript
149
- * logger.trace('{*}', () => ({
150
- * method: expensiveMethod(),
151
- * url: expensiveUrl(),
152
- * }));
153
- * ```
154
- *
155
- * @param properties The values to log. Note that this does not take
156
- * a callback.
157
- * @since 0.12.0
158
- */
159
- trace(properties: Record<string, unknown>): void;
160
-
161
- /**
162
- * Lazily log a trace message. Use this when the message values are expensive
163
- * to compute and should only be computed if the message is actually logged.
164
- *
165
- * ```typescript
166
- * logger.trace(l => l`A trace message with ${expensiveValue()}.`);
167
- * ```
168
- *
169
- * @param callback A callback that returns the message template prefix.
170
- * @throws {TypeError} If no log record was made inside the callback.
171
- * @since 0.12.0
172
- */
173
- trace(callback: LogCallback): void;
174
-
175
- /**
176
- * Log a debug message. Use this as a template string prefix.
177
- *
178
- * ```typescript
179
- * logger.debug `A debug message with ${value}.`;
180
- * ```
181
- *
182
- * @param message The message template strings array.
183
- * @param values The message template values.
184
- */
185
- debug(message: TemplateStringsArray, ...values: readonly unknown[]): void;
186
-
187
- /**
188
- * Log a debug message with properties.
189
- *
190
- * ```typescript
191
- * logger.debug('A debug message with {value}.', { value });
192
- * ```
193
- *
194
- * If the properties are expensive to compute, you can pass a callback that
195
- * returns the properties:
196
- *
197
- * ```typescript
198
- * logger.debug(
199
- * 'A debug message with {value}.',
200
- * () => ({ value: expensiveComputation() })
201
- * );
202
- * ```
203
- *
204
- * @param message The message template. Placeholders to be replaced with
205
- * `values` are indicated by keys in curly braces (e.g.,
206
- * `{value}`).
207
- * @param properties The values to replace placeholders with. For lazy
208
- * evaluation, this can be a callback that returns the
209
- * properties.
210
- */
211
- debug(
212
- message: string,
213
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
214
- ): void;
215
-
216
- /**
217
- * Log a debug values with no message. This is useful when you
218
- * want to log properties without a message, e.g., when you want to log
219
- * the context of a request or an operation.
220
- *
221
- * ```typescript
222
- * logger.debug({ method: 'GET', url: '/api/v1/resource' });
223
- * ```
224
- *
225
- * Note that this is a shorthand for:
226
- *
227
- * ```typescript
228
- * logger.debug('{*}', { method: 'GET', url: '/api/v1/resource' });
229
- * ```
230
- *
231
- * If the properties are expensive to compute, you cannot use this shorthand
232
- * and should use the following syntax instead:
233
- *
234
- * ```typescript
235
- * logger.debug('{*}', () => ({
236
- * method: expensiveMethod(),
237
- * url: expensiveUrl(),
238
- * }));
239
- * ```
240
- *
241
- * @param properties The values to log. Note that this does not take
242
- * a callback.
243
- * @since 0.11.0
244
- */
245
- debug(properties: Record<string, unknown>): void;
246
-
247
- /**
248
- * Lazily log a debug message. Use this when the message values are expensive
249
- * to compute and should only be computed if the message is actually logged.
250
- *
251
- * ```typescript
252
- * logger.debug(l => l`A debug message with ${expensiveValue()}.`);
253
- * ```
254
- *
255
- * @param callback A callback that returns the message template prefix.
256
- * @throws {TypeError} If no log record was made inside the callback.
257
- */
258
- debug(callback: LogCallback): void;
259
-
260
- /**
261
- * Log an informational message. Use this as a template string prefix.
262
- *
263
- * ```typescript
264
- * logger.info `An info message with ${value}.`;
265
- * ```
266
- *
267
- * @param message The message template strings array.
268
- * @param values The message template values.
269
- */
270
- info(message: TemplateStringsArray, ...values: readonly unknown[]): void;
271
-
272
- /**
273
- * Log an informational message with properties.
274
- *
275
- * ```typescript
276
- * logger.info('An info message with {value}.', { value });
277
- * ```
278
- *
279
- * If the properties are expensive to compute, you can pass a callback that
280
- * returns the properties:
281
- *
282
- * ```typescript
283
- * logger.info(
284
- * 'An info message with {value}.',
285
- * () => ({ value: expensiveComputation() })
286
- * );
287
- * ```
288
- *
289
- * @param message The message template. Placeholders to be replaced with
290
- * `values` are indicated by keys in curly braces (e.g.,
291
- * `{value}`).
292
- * @param properties The values to replace placeholders with. For lazy
293
- * evaluation, this can be a callback that returns the
294
- * properties.
295
- */
296
- info(
297
- message: string,
298
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
299
- ): void;
300
-
301
- /**
302
- * Log an informational values with no message. This is useful when you
303
- * want to log properties without a message, e.g., when you want to log
304
- * the context of a request or an operation.
305
- *
306
- * ```typescript
307
- * logger.info({ method: 'GET', url: '/api/v1/resource' });
308
- * ```
309
- *
310
- * Note that this is a shorthand for:
311
- *
312
- * ```typescript
313
- * logger.info('{*}', { method: 'GET', url: '/api/v1/resource' });
314
- * ```
315
- *
316
- * If the properties are expensive to compute, you cannot use this shorthand
317
- * and should use the following syntax instead:
318
- *
319
- * ```typescript
320
- * logger.info('{*}', () => ({
321
- * method: expensiveMethod(),
322
- * url: expensiveUrl(),
323
- * }));
324
- * ```
325
- *
326
- * @param properties The values to log. Note that this does not take
327
- * a callback.
328
- * @since 0.11.0
329
- */
330
- info(properties: Record<string, unknown>): void;
331
-
332
- /**
333
- * Lazily log an informational message. Use this when the message values are
334
- * expensive to compute and should only be computed if the message is actually
335
- * logged.
336
- *
337
- * ```typescript
338
- * logger.info(l => l`An info message with ${expensiveValue()}.`);
339
- * ```
340
- *
341
- * @param callback A callback that returns the message template prefix.
342
- * @throws {TypeError} If no log record was made inside the callback.
343
- */
344
- info(callback: LogCallback): void;
345
-
346
- /**
347
- * Log a warning message. Use this as a template string prefix.
348
- *
349
- * ```typescript
350
- * logger.warn `A warning message with ${value}.`;
351
- * ```
352
- *
353
- * @param message The message template strings array.
354
- * @param values The message template values.
355
- */
356
- warn(message: TemplateStringsArray, ...values: readonly unknown[]): void;
357
-
358
- /**
359
- * Log a warning message with properties.
360
- *
361
- * ```typescript
362
- * logger.warn('A warning message with {value}.', { value });
363
- * ```
364
- *
365
- * If the properties are expensive to compute, you can pass a callback that
366
- * returns the properties:
367
- *
368
- * ```typescript
369
- * logger.warn(
370
- * 'A warning message with {value}.',
371
- * () => ({ value: expensiveComputation() })
372
- * );
373
- * ```
374
- *
375
- * @param message The message template. Placeholders to be replaced with
376
- * `values` are indicated by keys in curly braces (e.g.,
377
- * `{value}`).
378
- * @param properties The values to replace placeholders with. For lazy
379
- * evaluation, this can be a callback that returns the
380
- * properties.
381
- */
382
- warn(
383
- message: string,
384
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
385
- ): void;
386
-
387
- /**
388
- * Log a warning values with no message. This is useful when you
389
- * want to log properties without a message, e.g., when you want to log
390
- * the context of a request or an operation.
391
- *
392
- * ```typescript
393
- * logger.warn({ method: 'GET', url: '/api/v1/resource' });
394
- * ```
395
- *
396
- * Note that this is a shorthand for:
397
- *
398
- * ```typescript
399
- * logger.warn('{*}', { method: 'GET', url: '/api/v1/resource' });
400
- * ```
401
- *
402
- * If the properties are expensive to compute, you cannot use this shorthand
403
- * and should use the following syntax instead:
404
- *
405
- * ```typescript
406
- * logger.warn('{*}', () => ({
407
- * method: expensiveMethod(),
408
- * url: expensiveUrl(),
409
- * }));
410
- * ```
411
- *
412
- * @param properties The values to log. Note that this does not take
413
- * a callback.
414
- * @since 0.11.0
415
- */
416
- warn(properties: Record<string, unknown>): void;
417
-
418
- /**
419
- * Lazily log a warning message. Use this when the message values are
420
- * expensive to compute and should only be computed if the message is actually
421
- * logged.
422
- *
423
- * ```typescript
424
- * logger.warn(l => l`A warning message with ${expensiveValue()}.`);
425
- * ```
426
- *
427
- * @param callback A callback that returns the message template prefix.
428
- * @throws {TypeError} If no log record was made inside the callback.
429
- */
430
- warn(callback: LogCallback): void;
431
-
432
- /**
433
- * Log a warning message. Use this as a template string prefix.
434
- *
435
- * ```typescript
436
- * logger.warning `A warning message with ${value}.`;
437
- * ```
438
- *
439
- * @param message The message template strings array.
440
- * @param values The message template values.
441
- * @since 0.12.0
442
- */
443
- warning(message: TemplateStringsArray, ...values: readonly unknown[]): void;
444
-
445
- /**
446
- * Log a warning message with properties.
447
- *
448
- * ```typescript
449
- * logger.warning('A warning message with {value}.', { value });
450
- * ```
451
- *
452
- * If the properties are expensive to compute, you can pass a callback that
453
- * returns the properties:
454
- *
455
- * ```typescript
456
- * logger.warning(
457
- * 'A warning message with {value}.',
458
- * () => ({ value: expensiveComputation() })
459
- * );
460
- * ```
461
- *
462
- * @param message The message template. Placeholders to be replaced with
463
- * `values` are indicated by keys in curly braces (e.g.,
464
- * `{value}`).
465
- * @param properties The values to replace placeholders with. For lazy
466
- * evaluation, this can be a callback that returns the
467
- * properties.
468
- * @since 0.12.0
469
- */
470
- warning(
471
- message: string,
472
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
473
- ): void;
474
-
475
- /**
476
- * Log a warning values with no message. This is useful when you
477
- * want to log properties without a message, e.g., when you want to log
478
- * the context of a request or an operation.
479
- *
480
- * ```typescript
481
- * logger.warning({ method: 'GET', url: '/api/v1/resource' });
482
- * ```
483
- *
484
- * Note that this is a shorthand for:
485
- *
486
- * ```typescript
487
- * logger.warning('{*}', { method: 'GET', url: '/api/v1/resource' });
488
- * ```
489
- *
490
- * If the properties are expensive to compute, you cannot use this shorthand
491
- * and should use the following syntax instead:
492
- *
493
- * ```typescript
494
- * logger.warning('{*}', () => ({
495
- * method: expensiveMethod(),
496
- * url: expensiveUrl(),
497
- * }));
498
- * ```
499
- *
500
- * @param properties The values to log. Note that this does not take
501
- * a callback.
502
- * @since 0.12.0
503
- */
504
- warning(properties: Record<string, unknown>): void;
505
-
506
- /**
507
- * Lazily log a warning message. Use this when the message values are
508
- * expensive to compute and should only be computed if the message is actually
509
- * logged.
510
- *
511
- * ```typescript
512
- * logger.warning(l => l`A warning message with ${expensiveValue()}.`);
513
- * ```
514
- *
515
- * @param callback A callback that returns the message template prefix.
516
- * @throws {TypeError} If no log record was made inside the callback.
517
- * @since 0.12.0
518
- */
519
- warning(callback: LogCallback): void;
520
-
521
- /**
522
- * Log an error message. Use this as a template string prefix.
523
- *
524
- * ```typescript
525
- * logger.error `An error message with ${value}.`;
526
- * ```
527
- *
528
- * @param message The message template strings array.
529
- * @param values The message template values.
530
- */
531
- error(message: TemplateStringsArray, ...values: readonly unknown[]): void;
532
-
533
- /**
534
- * Log an error message with properties.
535
- *
536
- * ```typescript
537
- * logger.warn('An error message with {value}.', { value });
538
- * ```
539
- *
540
- * If the properties are expensive to compute, you can pass a callback that
541
- * returns the properties:
542
- *
543
- * ```typescript
544
- * logger.error(
545
- * 'An error message with {value}.',
546
- * () => ({ value: expensiveComputation() })
547
- * );
548
- * ```
549
- *
550
- * @param message The message template. Placeholders to be replaced with
551
- * `values` are indicated by keys in curly braces (e.g.,
552
- * `{value}`).
553
- * @param properties The values to replace placeholders with. For lazy
554
- * evaluation, this can be a callback that returns the
555
- * properties.
556
- */
557
- error(
558
- message: string,
559
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
560
- ): void;
561
-
562
- /**
563
- * Log an error values with no message. This is useful when you
564
- * want to log properties without a message, e.g., when you want to log
565
- * the context of a request or an operation.
566
- *
567
- * ```typescript
568
- * logger.error({ method: 'GET', url: '/api/v1/resource' });
569
- * ```
570
- *
571
- * Note that this is a shorthand for:
572
- *
573
- * ```typescript
574
- * logger.error('{*}', { method: 'GET', url: '/api/v1/resource' });
575
- * ```
576
- *
577
- * If the properties are expensive to compute, you cannot use this shorthand
578
- * and should use the following syntax instead:
579
- *
580
- * ```typescript
581
- * logger.error('{*}', () => ({
582
- * method: expensiveMethod(),
583
- * url: expensiveUrl(),
584
- * }));
585
- * ```
586
- *
587
- * @param properties The values to log. Note that this does not take
588
- * a callback.
589
- * @since 0.11.0
590
- */
591
- error(properties: Record<string, unknown>): void;
592
-
593
- /**
594
- * Lazily log an error message. Use this when the message values are
595
- * expensive to compute and should only be computed if the message is actually
596
- * logged.
597
- *
598
- * ```typescript
599
- * logger.error(l => l`An error message with ${expensiveValue()}.`);
600
- * ```
601
- *
602
- * @param callback A callback that returns the message template prefix.
603
- * @throws {TypeError} If no log record was made inside the callback.
604
- */
605
- error(callback: LogCallback): void;
606
-
607
- /**
608
- * Log a fatal error message. Use this as a template string prefix.
609
- *
610
- * ```typescript
611
- * logger.fatal `A fatal error message with ${value}.`;
612
- * ```
613
- *
614
- * @param message The message template strings array.
615
- * @param values The message template values.
616
- */
617
- fatal(message: TemplateStringsArray, ...values: readonly unknown[]): void;
618
-
619
- /**
620
- * Log a fatal error message with properties.
621
- *
622
- * ```typescript
623
- * logger.warn('A fatal error message with {value}.', { value });
624
- * ```
625
- *
626
- * If the properties are expensive to compute, you can pass a callback that
627
- * returns the properties:
628
- *
629
- * ```typescript
630
- * logger.fatal(
631
- * 'A fatal error message with {value}.',
632
- * () => ({ value: expensiveComputation() })
633
- * );
634
- * ```
635
- *
636
- * @param message The message template. Placeholders to be replaced with
637
- * `values` are indicated by keys in curly braces (e.g.,
638
- * `{value}`).
639
- * @param properties The values to replace placeholders with. For lazy
640
- * evaluation, this can be a callback that returns the
641
- * properties.
642
- */
643
- fatal(
644
- message: string,
645
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
646
- ): void;
647
-
648
- /**
649
- * Log a fatal error values with no message. This is useful when you
650
- * want to log properties without a message, e.g., when you want to log
651
- * the context of a request or an operation.
652
- *
653
- * ```typescript
654
- * logger.fatal({ method: 'GET', url: '/api/v1/resource' });
655
- * ```
656
- *
657
- * Note that this is a shorthand for:
658
- *
659
- * ```typescript
660
- * logger.fatal('{*}', { method: 'GET', url: '/api/v1/resource' });
661
- * ```
662
- *
663
- * If the properties are expensive to compute, you cannot use this shorthand
664
- * and should use the following syntax instead:
665
- *
666
- * ```typescript
667
- * logger.fatal('{*}', () => ({
668
- * method: expensiveMethod(),
669
- * url: expensiveUrl(),
670
- * }));
671
- * ```
672
- *
673
- * @param properties The values to log. Note that this does not take
674
- * a callback.
675
- * @since 0.11.0
676
- */
677
- fatal(properties: Record<string, unknown>): void;
678
-
679
- /**
680
- * Lazily log a fatal error message. Use this when the message values are
681
- * expensive to compute and should only be computed if the message is actually
682
- * logged.
683
- *
684
- * ```typescript
685
- * logger.fatal(l => l`A fatal error message with ${expensiveValue()}.`);
686
- * ```
687
- *
688
- * @param callback A callback that returns the message template prefix.
689
- * @throws {TypeError} If no log record was made inside the callback.
690
- */
691
- fatal(callback: LogCallback): void;
692
-
693
- /**
694
- * Emits a log record with custom fields while using this logger's
695
- * category.
696
- *
697
- * This is a low-level API for integration scenarios where you need full
698
- * control over the log record, particularly for preserving timestamps
699
- * from external systems.
700
- *
701
- * ```typescript
702
- * const logger = getLogger(["my-app", "integration"]);
703
- *
704
- * // Emit a log with a custom timestamp
705
- * logger.emit({
706
- * timestamp: kafkaLog.originalTimestamp,
707
- * level: "info",
708
- * message: [kafkaLog.message],
709
- * rawMessage: kafkaLog.message,
710
- * properties: {
711
- * source: "kafka",
712
- * partition: kafkaLog.partition,
713
- * offset: kafkaLog.offset,
714
- * },
715
- * });
716
- * ```
717
- *
718
- * @param record Log record without category field (category comes from
719
- * the logger instance)
720
- * @since 1.1.0
721
- */
722
- emit(record: Omit<LogRecord, "category">): void;
723
- }
724
-
725
- /**
726
- * A logging callback function. It is used to defer the computation of a
727
- * message template until it is actually logged.
728
- * @param prefix The message template prefix.
729
- * @returns The rendered message array.
730
- */
731
- export type LogCallback = (prefix: LogTemplatePrefix) => unknown[];
732
-
733
- /**
734
- * A logging template prefix function. It is used to log a message in
735
- * a {@link LogCallback} function.
736
- * @param message The message template strings array.
737
- * @param values The message template values.
738
- * @returns The rendered message array.
739
- */
740
- export type LogTemplatePrefix = (
741
- message: TemplateStringsArray,
742
- ...values: unknown[]
743
- ) => unknown[];
744
-
745
- /**
746
- * A function type for logging methods in the {@link Logger} interface.
747
- * @since 1.0.0
748
- */
749
- export interface LogMethod {
750
- /**
751
- * Log a message with the given level using a template string.
752
- * @param message The message template strings array.
753
- * @param values The message template values.
754
- */
755
- (
756
- message: TemplateStringsArray,
757
- ...values: readonly unknown[]
758
- ): void;
759
-
760
- /**
761
- * Log a message with the given level with properties.
762
- * @param message The message template. Placeholders to be replaced with
763
- * `values` are indicated by keys in curly braces (e.g.,
764
- * `{value}`).
765
- * @param properties The values to replace placeholders with. For lazy
766
- * evaluation, this can be a callback that returns the
767
- * properties.
768
- */
769
- (
770
- message: string,
771
- properties?: Record<string, unknown> | (() => Record<string, unknown>),
772
- ): void;
773
-
774
- /**
775
- * Log a message with the given level with no message.
776
- * @param properties The values to log. Note that this does not take
777
- * a callback.
778
- */
779
- (properties: Record<string, unknown>): void;
780
-
781
- /**
782
- * Lazily log a message with the given level.
783
- * @param callback A callback that returns the message template prefix.
784
- * @throws {TypeError} If no log record was made inside the callback.
785
- */
786
- (callback: LogCallback): void;
787
- }
788
-
789
- /**
790
- * Get a logger with the given category.
791
- *
792
- * ```typescript
793
- * const logger = getLogger(["my-app"]);
794
- * ```
795
- *
796
- * @param category The category of the logger. It can be a string or an array
797
- * of strings. If it is a string, it is equivalent to an array
798
- * with a single element.
799
- * @returns The logger.
800
- */
801
- export function getLogger(category: string | readonly string[] = []): Logger {
802
- return LoggerImpl.getLogger(category);
803
- }
804
-
805
- /**
806
- * The symbol for the global root logger.
807
- */
808
- const globalRootLoggerSymbol = Symbol.for("logtape.rootLogger");
809
-
810
- /**
811
- * The global root logger registry.
812
- */
813
- interface GlobalRootLoggerRegistry {
814
- [globalRootLoggerSymbol]?: LoggerImpl;
815
- }
816
-
817
- /**
818
- * A logger implementation. Do not use this directly; use {@link getLogger}
819
- * instead. This class is exported for testing purposes.
820
- */
821
- export class LoggerImpl implements Logger {
822
- readonly parent: LoggerImpl | null;
823
- readonly children: Record<string, LoggerImpl | WeakRef<LoggerImpl>>;
824
- readonly category: readonly string[];
825
- readonly sinks: Sink[];
826
- parentSinks: "inherit" | "override" = "inherit";
827
- readonly filters: Filter[];
828
- lowestLevel: LogLevel | null = "trace";
829
- contextLocalStorage?: ContextLocalStorage<Record<string, unknown>>;
830
-
831
- static getLogger(category: string | readonly string[] = []): LoggerImpl {
832
- let rootLogger: LoggerImpl | null = globalRootLoggerSymbol in globalThis
833
- ? ((globalThis as GlobalRootLoggerRegistry)[globalRootLoggerSymbol] ??
834
- null)
835
- : null;
836
- if (rootLogger == null) {
837
- rootLogger = new LoggerImpl(null, []);
838
- (globalThis as GlobalRootLoggerRegistry)[globalRootLoggerSymbol] =
839
- rootLogger;
840
- }
841
- if (typeof category === "string") return rootLogger.getChild(category);
842
- if (category.length === 0) return rootLogger;
843
- return rootLogger.getChild(category as readonly [string, ...string[]]);
844
- }
845
-
846
- private constructor(parent: LoggerImpl | null, category: readonly string[]) {
847
- this.parent = parent;
848
- this.children = {};
849
- this.category = category;
850
- this.sinks = [];
851
- this.filters = [];
852
- }
853
-
854
- getChild(
855
- subcategory:
856
- | string
857
- | readonly [string]
858
- | readonly [string, ...(readonly string[])],
859
- ): LoggerImpl {
860
- const name = typeof subcategory === "string" ? subcategory : subcategory[0];
861
- const childRef = this.children[name];
862
- let child: LoggerImpl | undefined = childRef instanceof LoggerImpl
863
- ? childRef
864
- : childRef?.deref();
865
- if (child == null) {
866
- child = new LoggerImpl(this, [...this.category, name]);
867
- this.children[name] = "WeakRef" in globalThis
868
- ? new WeakRef(child)
869
- : child;
870
- }
871
- if (typeof subcategory === "string" || subcategory.length === 1) {
872
- return child;
873
- }
874
- return child.getChild(
875
- subcategory.slice(1) as [string, ...(readonly string[])],
876
- );
877
- }
878
-
879
- /**
880
- * Reset the logger. This removes all sinks and filters from the logger.
881
- */
882
- reset(): void {
883
- while (this.sinks.length > 0) this.sinks.shift();
884
- this.parentSinks = "inherit";
885
- while (this.filters.length > 0) this.filters.shift();
886
- this.lowestLevel = "trace";
887
- }
888
-
889
- /**
890
- * Reset the logger and all its descendants. This removes all sinks and
891
- * filters from the logger and all its descendants.
892
- */
893
- resetDescendants(): void {
894
- for (const child of Object.values(this.children)) {
895
- const logger = child instanceof LoggerImpl ? child : child.deref();
896
- if (logger != null) logger.resetDescendants();
897
- }
898
- this.reset();
899
- }
900
-
901
- with(properties: Record<string, unknown>): Logger {
902
- return new LoggerCtx(this, { ...properties });
903
- }
904
-
905
- filter(record: LogRecord): boolean {
906
- for (const filter of this.filters) {
907
- if (!filter(record)) return false;
908
- }
909
- if (this.filters.length < 1) return this.parent?.filter(record) ?? true;
910
- return true;
911
- }
912
-
913
- *getSinks(level: LogLevel): Iterable<Sink> {
914
- if (
915
- this.lowestLevel === null || compareLogLevel(level, this.lowestLevel) < 0
916
- ) {
917
- return;
918
- }
919
- if (this.parent != null && this.parentSinks === "inherit") {
920
- for (const sink of this.parent.getSinks(level)) yield sink;
921
- }
922
- for (const sink of this.sinks) yield sink;
923
- }
924
-
925
- emit(record: Omit<LogRecord, "category">): void;
926
- emit(record: LogRecord, bypassSinks?: Set<Sink>): void;
927
- emit(
928
- record: Omit<LogRecord, "category"> | LogRecord,
929
- bypassSinks?: Set<Sink>,
930
- ): void {
931
- const categoryPrefix = getCategoryPrefix();
932
- const baseCategory = "category" in record
933
- ? (record as LogRecord).category
934
- : this.category;
935
- const fullCategory = categoryPrefix.length > 0
936
- ? [...categoryPrefix, ...baseCategory]
937
- : baseCategory;
938
-
939
- // Create the full record by copying property descriptors from the original
940
- // record, which preserves getters without invoking them (unlike spread).
941
- const descriptors = Object.getOwnPropertyDescriptors(record) as
942
- & PropertyDescriptorMap
943
- & { category?: PropertyDescriptor };
944
- descriptors.category = {
945
- value: fullCategory,
946
- enumerable: true,
947
- configurable: true,
948
- };
949
- const fullRecord = Object.defineProperties({}, descriptors) as LogRecord;
950
-
951
- if (
952
- this.lowestLevel === null ||
953
- compareLogLevel(fullRecord.level, this.lowestLevel) < 0 ||
954
- !this.filter(fullRecord)
955
- ) {
956
- return;
957
- }
958
- for (const sink of this.getSinks(fullRecord.level)) {
959
- if (bypassSinks?.has(sink)) continue;
960
- try {
961
- sink(fullRecord);
962
- } catch (error) {
963
- const bypassSinks2 = new Set(bypassSinks);
964
- bypassSinks2.add(sink);
965
- metaLogger.log(
966
- "fatal",
967
- "Failed to emit a log record to sink {sink}: {error}",
968
- { sink, error, record: fullRecord },
969
- bypassSinks2,
970
- );
971
- }
972
- }
973
- }
974
-
975
- log(
976
- level: LogLevel,
977
- rawMessage: string,
978
- properties: Record<string, unknown> | (() => Record<string, unknown>),
979
- bypassSinks?: Set<Sink>,
980
- ): void {
981
- const implicitContext = getImplicitContext();
982
- let cachedProps: Record<string, unknown> | undefined = undefined;
983
- const record: LogRecord = typeof properties === "function"
984
- ? {
985
- category: this.category,
986
- level,
987
- timestamp: Date.now(),
988
- get message() {
989
- return parseMessageTemplate(rawMessage, this.properties);
990
- },
991
- rawMessage,
992
- get properties() {
993
- if (cachedProps == null) {
994
- cachedProps = {
995
- ...implicitContext,
996
- ...properties(),
997
- };
998
- }
999
- return cachedProps;
1000
- },
1001
- }
1002
- : {
1003
- category: this.category,
1004
- level,
1005
- timestamp: Date.now(),
1006
- message: parseMessageTemplate(rawMessage, {
1007
- ...implicitContext,
1008
- ...properties,
1009
- }),
1010
- rawMessage,
1011
- properties: { ...implicitContext, ...properties },
1012
- };
1013
- this.emit(record, bypassSinks);
1014
- }
1015
-
1016
- logLazily(
1017
- level: LogLevel,
1018
- callback: LogCallback,
1019
- properties: Record<string, unknown> = {},
1020
- ): void {
1021
- const implicitContext = getImplicitContext();
1022
- let rawMessage: TemplateStringsArray | undefined = undefined;
1023
- let msg: unknown[] | undefined = undefined;
1024
- function realizeMessage(): [unknown[], TemplateStringsArray] {
1025
- if (msg == null || rawMessage == null) {
1026
- msg = callback((tpl, ...values) => {
1027
- rawMessage = tpl;
1028
- return renderMessage(tpl, values);
1029
- });
1030
- if (rawMessage == null) throw new TypeError("No log record was made.");
1031
- }
1032
- return [msg, rawMessage];
1033
- }
1034
- this.emit({
1035
- category: this.category,
1036
- level,
1037
- get message() {
1038
- return realizeMessage()[0];
1039
- },
1040
- get rawMessage() {
1041
- return realizeMessage()[1];
1042
- },
1043
- timestamp: Date.now(),
1044
- properties: { ...implicitContext, ...properties },
1045
- });
1046
- }
1047
-
1048
- logTemplate(
1049
- level: LogLevel,
1050
- messageTemplate: TemplateStringsArray,
1051
- values: unknown[],
1052
- properties: Record<string, unknown> = {},
1053
- ): void {
1054
- const implicitContext = getImplicitContext();
1055
- this.emit({
1056
- category: this.category,
1057
- level,
1058
- message: renderMessage(messageTemplate, values),
1059
- rawMessage: messageTemplate,
1060
- timestamp: Date.now(),
1061
- properties: { ...implicitContext, ...properties },
1062
- });
1063
- }
1064
-
1065
- trace(
1066
- message:
1067
- | TemplateStringsArray
1068
- | string
1069
- | LogCallback
1070
- | Record<string, unknown>,
1071
- ...values: unknown[]
1072
- ): void {
1073
- if (typeof message === "string") {
1074
- this.log("trace", message, (values[0] ?? {}) as Record<string, unknown>);
1075
- } else if (typeof message === "function") {
1076
- this.logLazily("trace", message);
1077
- } else if (!Array.isArray(message)) {
1078
- this.log("trace", "{*}", message as Record<string, unknown>);
1079
- } else {
1080
- this.logTemplate("trace", message as TemplateStringsArray, values);
1081
- }
1082
- }
1083
-
1084
- debug(
1085
- message:
1086
- | TemplateStringsArray
1087
- | string
1088
- | LogCallback
1089
- | Record<string, unknown>,
1090
- ...values: unknown[]
1091
- ): void {
1092
- if (typeof message === "string") {
1093
- this.log("debug", message, (values[0] ?? {}) as Record<string, unknown>);
1094
- } else if (typeof message === "function") {
1095
- this.logLazily("debug", message);
1096
- } else if (!Array.isArray(message)) {
1097
- this.log("debug", "{*}", message as Record<string, unknown>);
1098
- } else {
1099
- this.logTemplate("debug", message as TemplateStringsArray, values);
1100
- }
1101
- }
1102
-
1103
- info(
1104
- message:
1105
- | TemplateStringsArray
1106
- | string
1107
- | LogCallback
1108
- | Record<string, unknown>,
1109
- ...values: unknown[]
1110
- ): void {
1111
- if (typeof message === "string") {
1112
- this.log("info", message, (values[0] ?? {}) as Record<string, unknown>);
1113
- } else if (typeof message === "function") {
1114
- this.logLazily("info", message);
1115
- } else if (!Array.isArray(message)) {
1116
- this.log("info", "{*}", message as Record<string, unknown>);
1117
- } else {
1118
- this.logTemplate("info", message as TemplateStringsArray, values);
1119
- }
1120
- }
1121
-
1122
- warn(
1123
- message:
1124
- | TemplateStringsArray
1125
- | string
1126
- | LogCallback
1127
- | Record<string, unknown>,
1128
- ...values: unknown[]
1129
- ): void {
1130
- if (typeof message === "string") {
1131
- this.log(
1132
- "warning",
1133
- message,
1134
- (values[0] ?? {}) as Record<string, unknown>,
1135
- );
1136
- } else if (typeof message === "function") {
1137
- this.logLazily("warning", message);
1138
- } else if (!Array.isArray(message)) {
1139
- this.log("warning", "{*}", message as Record<string, unknown>);
1140
- } else {
1141
- this.logTemplate("warning", message as TemplateStringsArray, values);
1142
- }
1143
- }
1144
-
1145
- warning(
1146
- message:
1147
- | TemplateStringsArray
1148
- | string
1149
- | LogCallback
1150
- | Record<string, unknown>,
1151
- ...values: unknown[]
1152
- ): void {
1153
- this.warn(message, ...values);
1154
- }
1155
-
1156
- error(
1157
- message:
1158
- | TemplateStringsArray
1159
- | string
1160
- | LogCallback
1161
- | Record<string, unknown>,
1162
- ...values: unknown[]
1163
- ): void {
1164
- if (typeof message === "string") {
1165
- this.log("error", message, (values[0] ?? {}) as Record<string, unknown>);
1166
- } else if (typeof message === "function") {
1167
- this.logLazily("error", message);
1168
- } else if (!Array.isArray(message)) {
1169
- this.log("error", "{*}", message as Record<string, unknown>);
1170
- } else {
1171
- this.logTemplate("error", message as TemplateStringsArray, values);
1172
- }
1173
- }
1174
-
1175
- fatal(
1176
- message:
1177
- | TemplateStringsArray
1178
- | string
1179
- | LogCallback
1180
- | Record<string, unknown>,
1181
- ...values: unknown[]
1182
- ): void {
1183
- if (typeof message === "string") {
1184
- this.log("fatal", message, (values[0] ?? {}) as Record<string, unknown>);
1185
- } else if (typeof message === "function") {
1186
- this.logLazily("fatal", message);
1187
- } else if (!Array.isArray(message)) {
1188
- this.log("fatal", "{*}", message as Record<string, unknown>);
1189
- } else {
1190
- this.logTemplate("fatal", message as TemplateStringsArray, values);
1191
- }
1192
- }
1193
- }
1194
-
1195
- /**
1196
- * A logger implementation with contextual properties. Do not use this
1197
- * directly; use {@link Logger.with} instead. This class is exported
1198
- * for testing purposes.
1199
- */
1200
- export class LoggerCtx implements Logger {
1201
- logger: LoggerImpl;
1202
- properties: Record<string, unknown>;
1203
-
1204
- constructor(logger: LoggerImpl, properties: Record<string, unknown>) {
1205
- this.logger = logger;
1206
- this.properties = properties;
1207
- }
1208
-
1209
- get category(): readonly string[] {
1210
- return this.logger.category;
1211
- }
1212
-
1213
- get parent(): Logger | null {
1214
- return this.logger.parent;
1215
- }
1216
-
1217
- getChild(
1218
- subcategory: string | readonly [string] | readonly [string, ...string[]],
1219
- ): Logger {
1220
- return this.logger.getChild(subcategory).with(this.properties);
1221
- }
1222
-
1223
- with(properties: Record<string, unknown>): Logger {
1224
- return new LoggerCtx(this.logger, { ...this.properties, ...properties });
1225
- }
1226
-
1227
- log(
1228
- level: LogLevel,
1229
- message: string,
1230
- properties: Record<string, unknown> | (() => Record<string, unknown>),
1231
- bypassSinks?: Set<Sink>,
1232
- ): void {
1233
- this.logger.log(
1234
- level,
1235
- message,
1236
- typeof properties === "function"
1237
- ? () => ({
1238
- ...this.properties,
1239
- ...properties(),
1240
- })
1241
- : { ...this.properties, ...properties },
1242
- bypassSinks,
1243
- );
1244
- }
1245
-
1246
- logLazily(level: LogLevel, callback: LogCallback): void {
1247
- this.logger.logLazily(level, callback, this.properties);
1248
- }
1249
-
1250
- logTemplate(
1251
- level: LogLevel,
1252
- messageTemplate: TemplateStringsArray,
1253
- values: unknown[],
1254
- ): void {
1255
- this.logger.logTemplate(level, messageTemplate, values, this.properties);
1256
- }
1257
-
1258
- emit(record: Omit<LogRecord, "category">): void {
1259
- const recordWithContext = {
1260
- ...record,
1261
- properties: { ...this.properties, ...record.properties },
1262
- };
1263
- this.logger.emit(recordWithContext);
1264
- }
1265
-
1266
- trace(
1267
- message:
1268
- | TemplateStringsArray
1269
- | string
1270
- | LogCallback
1271
- | Record<string, unknown>,
1272
- ...values: unknown[]
1273
- ): void {
1274
- if (typeof message === "string") {
1275
- this.log("trace", message, (values[0] ?? {}) as Record<string, unknown>);
1276
- } else if (typeof message === "function") {
1277
- this.logLazily("trace", message);
1278
- } else if (!Array.isArray(message)) {
1279
- this.log("trace", "{*}", message as Record<string, unknown>);
1280
- } else {
1281
- this.logTemplate("trace", message as TemplateStringsArray, values);
1282
- }
1283
- }
1284
-
1285
- debug(
1286
- message:
1287
- | TemplateStringsArray
1288
- | string
1289
- | LogCallback
1290
- | Record<string, unknown>,
1291
- ...values: unknown[]
1292
- ): void {
1293
- if (typeof message === "string") {
1294
- this.log("debug", message, (values[0] ?? {}) as Record<string, unknown>);
1295
- } else if (typeof message === "function") {
1296
- this.logLazily("debug", message);
1297
- } else if (!Array.isArray(message)) {
1298
- this.log("debug", "{*}", message as Record<string, unknown>);
1299
- } else {
1300
- this.logTemplate("debug", message as TemplateStringsArray, values);
1301
- }
1302
- }
1303
-
1304
- info(
1305
- message:
1306
- | TemplateStringsArray
1307
- | string
1308
- | LogCallback
1309
- | Record<string, unknown>,
1310
- ...values: unknown[]
1311
- ): void {
1312
- if (typeof message === "string") {
1313
- this.log("info", message, (values[0] ?? {}) as Record<string, unknown>);
1314
- } else if (typeof message === "function") {
1315
- this.logLazily("info", message);
1316
- } else if (!Array.isArray(message)) {
1317
- this.log("info", "{*}", message as Record<string, unknown>);
1318
- } else {
1319
- this.logTemplate("info", message as TemplateStringsArray, values);
1320
- }
1321
- }
1322
-
1323
- warn(
1324
- message:
1325
- | TemplateStringsArray
1326
- | string
1327
- | LogCallback
1328
- | Record<string, unknown>,
1329
- ...values: unknown[]
1330
- ): void {
1331
- if (typeof message === "string") {
1332
- this.log(
1333
- "warning",
1334
- message,
1335
- (values[0] ?? {}) as Record<string, unknown>,
1336
- );
1337
- } else if (typeof message === "function") {
1338
- this.logLazily("warning", message);
1339
- } else if (!Array.isArray(message)) {
1340
- this.log("warning", "{*}", message as Record<string, unknown>);
1341
- } else {
1342
- this.logTemplate("warning", message as TemplateStringsArray, values);
1343
- }
1344
- }
1345
-
1346
- warning(
1347
- message:
1348
- | TemplateStringsArray
1349
- | string
1350
- | LogCallback
1351
- | Record<string, unknown>,
1352
- ...values: unknown[]
1353
- ): void {
1354
- this.warn(message, ...values);
1355
- }
1356
-
1357
- error(
1358
- message:
1359
- | TemplateStringsArray
1360
- | string
1361
- | LogCallback
1362
- | Record<string, unknown>,
1363
- ...values: unknown[]
1364
- ): void {
1365
- if (typeof message === "string") {
1366
- this.log("error", message, (values[0] ?? {}) as Record<string, unknown>);
1367
- } else if (typeof message === "function") {
1368
- this.logLazily("error", message);
1369
- } else if (!Array.isArray(message)) {
1370
- this.log("error", "{*}", message as Record<string, unknown>);
1371
- } else {
1372
- this.logTemplate("error", message as TemplateStringsArray, values);
1373
- }
1374
- }
1375
-
1376
- fatal(
1377
- message:
1378
- | TemplateStringsArray
1379
- | string
1380
- | LogCallback
1381
- | Record<string, unknown>,
1382
- ...values: unknown[]
1383
- ): void {
1384
- if (typeof message === "string") {
1385
- this.log("fatal", message, (values[0] ?? {}) as Record<string, unknown>);
1386
- } else if (typeof message === "function") {
1387
- this.logLazily("fatal", message);
1388
- } else if (!Array.isArray(message)) {
1389
- this.log("fatal", "{*}", message as Record<string, unknown>);
1390
- } else {
1391
- this.logTemplate("fatal", message as TemplateStringsArray, values);
1392
- }
1393
- }
1394
- }
1395
-
1396
- /**
1397
- * The meta logger. It is a logger with the category `["logtape", "meta"]`.
1398
- */
1399
- const metaLogger = LoggerImpl.getLogger(["logtape", "meta"]);
1400
-
1401
- /**
1402
- * Check if a property access key contains nested access patterns.
1403
- * @param key The property key to check.
1404
- * @returns True if the key contains nested access patterns.
1405
- */
1406
- function isNestedAccess(key: string): boolean {
1407
- return key.includes(".") || key.includes("[") || key.includes("?.");
1408
- }
1409
-
1410
- /**
1411
- * Safely access an own property from an object, blocking prototype pollution.
1412
- *
1413
- * @param obj The object to access the property from.
1414
- * @param key The property key to access.
1415
- * @returns The property value or undefined if not accessible.
1416
- */
1417
- function getOwnProperty(obj: unknown, key: string): unknown {
1418
- // Block dangerous prototype keys
1419
- if (key === "__proto__" || key === "prototype" || key === "constructor") {
1420
- return undefined;
1421
- }
1422
-
1423
- if ((typeof obj === "object" || typeof obj === "function") && obj !== null) {
1424
- return Object.prototype.hasOwnProperty.call(obj, key)
1425
- ? (obj as Record<string, unknown>)[key]
1426
- : undefined;
1427
- }
1428
-
1429
- return undefined;
1430
- }
1431
-
1432
- /**
1433
- * Result of parsing a single segment from a property path.
1434
- */
1435
- interface ParseSegmentResult {
1436
- segment: string | number;
1437
- nextIndex: number;
1438
- }
1439
-
1440
- /**
1441
- * Parse the next segment from a property path string.
1442
- *
1443
- * @param path The full property path string.
1444
- * @param fromIndex The index to start parsing from.
1445
- * @returns The parsed segment and next index, or null if parsing fails.
1446
- */
1447
- function parseNextSegment(
1448
- path: string,
1449
- fromIndex: number,
1450
- ): ParseSegmentResult | null {
1451
- const len = path.length;
1452
- let i = fromIndex;
1453
-
1454
- if (i >= len) return null;
1455
-
1456
- let segment: string | number;
1457
-
1458
- if (path[i] === "[") {
1459
- // Bracket notation: [0] or ["prop"]
1460
- i++;
1461
- if (i >= len) return null;
1462
-
1463
- if (path[i] === '"' || path[i] === "'") {
1464
- // Quoted property name: ["prop-name"]
1465
- const quote = path[i];
1466
- i++;
1467
- // Build segment with proper escape handling
1468
- let segmentStr = "";
1469
- while (i < len && path[i] !== quote) {
1470
- if (path[i] === "\\") {
1471
- i++; // Skip backslash
1472
- if (i < len) {
1473
- // Handle escape sequences according to JavaScript spec
1474
- const escapeChar = path[i];
1475
- switch (escapeChar) {
1476
- case "n":
1477
- segmentStr += "\n";
1478
- break;
1479
- case "t":
1480
- segmentStr += "\t";
1481
- break;
1482
- case "r":
1483
- segmentStr += "\r";
1484
- break;
1485
- case "b":
1486
- segmentStr += "\b";
1487
- break;
1488
- case "f":
1489
- segmentStr += "\f";
1490
- break;
1491
- case "v":
1492
- segmentStr += "\v";
1493
- break;
1494
- case "0":
1495
- segmentStr += "\0";
1496
- break;
1497
- case "\\":
1498
- segmentStr += "\\";
1499
- break;
1500
- case '"':
1501
- segmentStr += '"';
1502
- break;
1503
- case "'":
1504
- segmentStr += "'";
1505
- break;
1506
- case "u":
1507
- // Unicode escape: \uXXXX
1508
- if (i + 4 < len) {
1509
- const hex = path.slice(i + 1, i + 5);
1510
- const codePoint = Number.parseInt(hex, 16);
1511
- if (!Number.isNaN(codePoint)) {
1512
- segmentStr += String.fromCharCode(codePoint);
1513
- i += 4; // Skip the 4 hex digits
1514
- } else {
1515
- // Invalid unicode escape, keep as-is
1516
- segmentStr += escapeChar;
1517
- }
1518
- } else {
1519
- // Not enough characters for unicode escape
1520
- segmentStr += escapeChar;
1521
- }
1522
- break;
1523
- default:
1524
- // For any other character after \, just add it as-is
1525
- segmentStr += escapeChar;
1526
- }
1527
- i++;
1528
- }
1529
- } else {
1530
- segmentStr += path[i];
1531
- i++;
1532
- }
1533
- }
1534
- if (i >= len) return null;
1535
- segment = segmentStr;
1536
- i++; // Skip closing quote
1537
- } else {
1538
- // Array index: [0]
1539
- const startIndex = i;
1540
- while (
1541
- i < len && path[i] !== "]" && path[i] !== "'" && path[i] !== '"'
1542
- ) {
1543
- i++;
1544
- }
1545
- if (i >= len) return null;
1546
- const indexStr = path.slice(startIndex, i);
1547
- // Empty bracket is invalid
1548
- if (indexStr.length === 0) return null;
1549
- const indexNum = Number(indexStr);
1550
- segment = Number.isNaN(indexNum) ? indexStr : indexNum;
1551
- }
1552
-
1553
- // Skip closing bracket
1554
- while (i < len && path[i] !== "]") i++;
1555
- if (i < len) i++;
1556
- } else {
1557
- // Dot notation: prop
1558
- const startIndex = i;
1559
- while (
1560
- i < len && path[i] !== "." && path[i] !== "[" && path[i] !== "?" &&
1561
- path[i] !== "]"
1562
- ) {
1563
- i++;
1564
- }
1565
- segment = path.slice(startIndex, i);
1566
- // Empty segment is invalid (e.g., leading dot, double dot, trailing dot)
1567
- if (segment.length === 0) return null;
1568
- }
1569
-
1570
- // Skip dot separator
1571
- if (i < len && path[i] === ".") i++;
1572
-
1573
- return { segment, nextIndex: i };
1574
- }
1575
-
1576
- /**
1577
- * Access a property or index on an object or array.
1578
- *
1579
- * @param obj The object or array to access.
1580
- * @param segment The property key or array index.
1581
- * @returns The accessed value or undefined if not accessible.
1582
- */
1583
- function accessProperty(obj: unknown, segment: string | number): unknown {
1584
- if (typeof segment === "string") {
1585
- return getOwnProperty(obj, segment);
1586
- }
1587
-
1588
- // Numeric index for arrays
1589
- if (Array.isArray(obj) && segment >= 0 && segment < obj.length) {
1590
- return obj[segment];
1591
- }
1592
-
1593
- return undefined;
1594
- }
1595
-
1596
- /**
1597
- * Resolve a nested property path from an object.
1598
- *
1599
- * There are two types of property access patterns:
1600
- * 1. Array/index access: [0] or ["prop"]
1601
- * 2. Property access: prop or prop?.next
1602
- *
1603
- * @param obj The object to traverse.
1604
- * @param path The property path (e.g., "user.name", "users[0].email", "user['full-name']").
1605
- * @returns The resolved value or undefined if path doesn't exist.
1606
- */
1607
- function resolvePropertyPath(obj: unknown, path: string): unknown {
1608
- if (obj == null) return undefined;
1609
-
1610
- // Check for invalid paths
1611
- if (path.length === 0 || path.endsWith(".")) return undefined;
1612
-
1613
- let current: unknown = obj;
1614
- let i = 0;
1615
- const len = path.length;
1616
-
1617
- while (i < len) {
1618
- // Handle optional chaining
1619
- const isOptional = path.slice(i, i + 2) === "?.";
1620
- if (isOptional) {
1621
- i += 2;
1622
- if (current == null) return undefined;
1623
- } else if (current == null) {
1624
- return undefined;
1625
- }
1626
-
1627
- // Parse the next segment
1628
- const result = parseNextSegment(path, i);
1629
- if (result === null) return undefined;
1630
-
1631
- const { segment, nextIndex } = result;
1632
- i = nextIndex;
1633
-
1634
- // Access the property/index
1635
- current = accessProperty(current, segment);
1636
- if (current === undefined) {
1637
- return undefined;
1638
- }
1639
- }
1640
-
1641
- return current;
1642
- }
1643
-
1644
- /**
1645
- * Parse a message template into a message template array and a values array.
1646
- *
1647
- * Placeholders to be replaced with `values` are indicated by keys in curly braces
1648
- * (e.g., `{value}`). The system supports both simple property access and nested
1649
- * property access patterns:
1650
- *
1651
- * **Simple property access:**
1652
- * ```ts
1653
- * parseMessageTemplate("Hello, {user}!", { user: "foo" })
1654
- * // Returns: ["Hello, ", "foo", "!"]
1655
- * ```
1656
- *
1657
- * **Nested property access (dot notation):**
1658
- * ```ts
1659
- * parseMessageTemplate("Hello, {user.name}!", {
1660
- * user: { name: "foo", email: "foo@example.com" }
1661
- * })
1662
- * // Returns: ["Hello, ", "foo", "!"]
1663
- * ```
1664
- *
1665
- * **Array indexing:**
1666
- * ```ts
1667
- * parseMessageTemplate("First: {users[0]}", {
1668
- * users: ["foo", "bar", "baz"]
1669
- * })
1670
- * // Returns: ["First: ", "foo", ""]
1671
- * ```
1672
- *
1673
- * **Bracket notation for special property names:**
1674
- * ```ts
1675
- * parseMessageTemplate("Name: {user[\"full-name\"]}", {
1676
- * user: { "full-name": "foo bar" }
1677
- * })
1678
- * // Returns: ["Name: ", "foo bar", ""]
1679
- * ```
1680
- *
1681
- * **Optional chaining for safe navigation:**
1682
- * ```ts
1683
- * parseMessageTemplate("Email: {user?.profile?.email}", {
1684
- * user: { name: "foo" }
1685
- * })
1686
- * // Returns: ["Email: ", undefined, ""]
1687
- * ```
1688
- *
1689
- * **Wildcard patterns:**
1690
- * - `{*}` - Replaced with the entire properties object
1691
- * - `{ key-with-whitespace }` - Whitespace is trimmed when looking up keys
1692
- *
1693
- * **Escaping:**
1694
- * - `{{` and `}}` are escaped literal braces
1695
- *
1696
- * **Error handling:**
1697
- * - Non-existent paths return `undefined`
1698
- * - Malformed expressions resolve to `undefined` without throwing errors
1699
- * - Out of bounds array access returns `undefined`
1700
- *
1701
- * @param template The message template string containing placeholders.
1702
- * @param properties The values to replace placeholders with.
1703
- * @returns The message template array with values interleaved between text segments.
1704
- */
1705
- export function parseMessageTemplate(
1706
- template: string,
1707
- properties: Record<string, unknown>,
1708
- ): readonly unknown[] {
1709
- const length = template.length;
1710
- if (length === 0) return [""];
1711
-
1712
- // Fast path: no placeholders
1713
- if (!template.includes("{")) return [template];
1714
-
1715
- const message: unknown[] = [];
1716
- let startIndex = 0;
1717
-
1718
- for (let i = 0; i < length; i++) {
1719
- const char = template[i];
1720
-
1721
- if (char === "{") {
1722
- const nextChar = i + 1 < length ? template[i + 1] : "";
1723
-
1724
- if (nextChar === "{") {
1725
- // Escaped { character - skip and continue
1726
- i++; // Skip the next {
1727
- continue;
1728
- }
1729
-
1730
- // Find the closing }
1731
- const closeIndex = template.indexOf("}", i + 1);
1732
- if (closeIndex === -1) {
1733
- // No closing } found, treat as literal text
1734
- continue;
1735
- }
1736
-
1737
- // Add text before placeholder
1738
- const beforeText = template.slice(startIndex, i);
1739
- message.push(beforeText.replace(/{{/g, "{").replace(/}}/g, "}"));
1740
-
1741
- // Extract and process placeholder key
1742
- const key = template.slice(i + 1, closeIndex);
1743
-
1744
- // Resolve property value
1745
- let prop: unknown;
1746
-
1747
- // Check for wildcard patterns
1748
- const trimmedKey = key.trim();
1749
- if (trimmedKey === "*") {
1750
- // This is a wildcard pattern
1751
- prop = key in properties
1752
- ? properties[key]
1753
- : "*" in properties
1754
- ? properties["*"]
1755
- : properties;
1756
- } else {
1757
- // Regular property lookup with possible whitespace handling
1758
- if (key !== trimmedKey) {
1759
- // Key has leading/trailing whitespace
1760
- prop = key in properties ? properties[key] : properties[trimmedKey];
1761
- } else {
1762
- // Key has no leading/trailing whitespace
1763
- prop = properties[key];
1764
- }
1765
-
1766
- // If property not found directly and this looks like nested access, try nested resolution
1767
- if (prop === undefined && isNestedAccess(trimmedKey)) {
1768
- prop = resolvePropertyPath(properties, trimmedKey);
1769
- }
1770
- }
1771
-
1772
- message.push(prop);
1773
- i = closeIndex; // Move to the }
1774
- startIndex = i + 1;
1775
- } else if (char === "}" && i + 1 < length && template[i + 1] === "}") {
1776
- // Escaped } character - skip
1777
- i++; // Skip the next }
1778
- }
1779
- }
1780
-
1781
- // Add remaining text
1782
- const remainingText = template.slice(startIndex);
1783
- message.push(remainingText.replace(/{{/g, "{").replace(/}}/g, "}"));
1784
-
1785
- return message;
1786
- }
1787
-
1788
- /**
1789
- * Render a message template with values.
1790
- * @param template The message template.
1791
- * @param values The message template values.
1792
- * @returns The message template values interleaved between the substitution
1793
- * values.
1794
- */
1795
- export function renderMessage(
1796
- template: TemplateStringsArray,
1797
- values: readonly unknown[],
1798
- ): unknown[] {
1799
- const args = [];
1800
- for (let i = 0; i < template.length; i++) {
1801
- args.push(template[i]);
1802
- if (i < values.length) args.push(values[i]);
1803
- }
1804
- return args;
1805
- }