nightingale 14.2.1 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +204 -57
  3. package/dist/definitions/config.d.ts +1 -1
  4. package/dist/definitions/debug/debug.d.ts +5 -0
  5. package/dist/definitions/debug/debug.d.ts.map +1 -0
  6. package/dist/definitions/debug/debug.test.d.ts +2 -0
  7. package/dist/definitions/debug/debug.test.d.ts.map +1 -0
  8. package/dist/definitions/formatter-utils/formatObject.d.ts +9 -0
  9. package/dist/definitions/formatter-utils/formatObject.d.ts.map +1 -0
  10. package/dist/definitions/formatter-utils/formatObject.test.d.ts +2 -0
  11. package/dist/definitions/formatter-utils/formatObject.test.d.ts.map +1 -0
  12. package/dist/definitions/formatter-utils/formatRecordToString.d.ts +4 -0
  13. package/dist/definitions/formatter-utils/formatRecordToString.d.ts.map +1 -0
  14. package/dist/definitions/formatter-utils/index.d.ts +15 -0
  15. package/dist/definitions/formatter-utils/index.d.ts.map +1 -0
  16. package/dist/definitions/formatter-utils/index.test.d.ts +2 -0
  17. package/dist/definitions/formatter-utils/index.test.d.ts.map +1 -0
  18. package/dist/definitions/formatter-utils/levelToStyles.d.ts +3 -0
  19. package/dist/definitions/formatter-utils/levelToStyles.d.ts.map +1 -0
  20. package/dist/definitions/formatter-utils/levelToSymbol.d.ts +3 -0
  21. package/dist/definitions/formatter-utils/levelToSymbol.d.ts.map +1 -0
  22. package/dist/definitions/formatter-utils/styleToHexColor.d.ts +7 -0
  23. package/dist/definitions/formatter-utils/styleToHexColor.d.ts.map +1 -0
  24. package/dist/definitions/formatter-utils/styleToHtmlStyle.d.ts +109 -0
  25. package/dist/definitions/formatter-utils/styleToHtmlStyle.d.ts.map +1 -0
  26. package/dist/definitions/formatters/ANSIFormatter.d.ts +6 -0
  27. package/dist/definitions/formatters/ANSIFormatter.d.ts.map +1 -0
  28. package/dist/definitions/formatters/ANSIFormatter.test.d.ts +2 -0
  29. package/dist/definitions/formatters/ANSIFormatter.test.d.ts.map +1 -0
  30. package/dist/definitions/formatters/BrowserConsoleFormatter.d.ts +10 -0
  31. package/dist/definitions/formatters/BrowserConsoleFormatter.d.ts.map +1 -0
  32. package/dist/definitions/formatters/BrowserConsoleFormatter.test.d.ts +2 -0
  33. package/dist/definitions/formatters/BrowserConsoleFormatter.test.d.ts.map +1 -0
  34. package/dist/definitions/formatters/HTMLFormatter.d.ts +5 -0
  35. package/dist/definitions/formatters/HTMLFormatter.d.ts.map +1 -0
  36. package/dist/definitions/formatters/HTMLFormatter.test.d.ts +2 -0
  37. package/dist/definitions/formatters/HTMLFormatter.test.d.ts.map +1 -0
  38. package/dist/definitions/formatters/JSONFormatter.d.ts +4 -0
  39. package/dist/definitions/formatters/JSONFormatter.d.ts.map +1 -0
  40. package/dist/definitions/formatters/JSONFormatter.test.d.ts +2 -0
  41. package/dist/definitions/formatters/JSONFormatter.test.d.ts.map +1 -0
  42. package/dist/definitions/formatters/MarkdownFormatter.d.ts +5 -0
  43. package/dist/definitions/formatters/MarkdownFormatter.d.ts.map +1 -0
  44. package/dist/definitions/formatters/MarkdownFormatter.test.d.ts +2 -0
  45. package/dist/definitions/formatters/MarkdownFormatter.test.d.ts.map +1 -0
  46. package/dist/definitions/formatters/RawFormatter.d.ts +5 -0
  47. package/dist/definitions/formatters/RawFormatter.d.ts.map +1 -0
  48. package/dist/definitions/formatters/RawFormatter.test.d.ts +2 -0
  49. package/dist/definitions/formatters/RawFormatter.test.d.ts.map +1 -0
  50. package/dist/definitions/handlers/BrowserConsoleHandler.d.ts +14 -0
  51. package/dist/definitions/handlers/BrowserConsoleHandler.d.ts.map +1 -0
  52. package/dist/definitions/handlers/ConsoleCLIHandler.d.ts +12 -0
  53. package/dist/definitions/handlers/ConsoleCLIHandler.d.ts.map +1 -0
  54. package/dist/definitions/handlers/ConsoleHandler.d.ts +14 -0
  55. package/dist/definitions/handlers/ConsoleHandler.d.ts.map +1 -0
  56. package/dist/definitions/handlers/StringHandler.d.ts +9 -0
  57. package/dist/definitions/handlers/StringHandler.d.ts.map +1 -0
  58. package/dist/definitions/index.d.ts +18 -4
  59. package/dist/definitions/index.d.ts.map +1 -1
  60. package/dist/definitions/loggers/LoggerCLI.d.ts +23 -0
  61. package/dist/definitions/loggers/LoggerCLI.d.ts.map +1 -0
  62. package/dist/definitions/outputs/cliConsoleOutput.d.ts +3 -0
  63. package/dist/definitions/outputs/cliConsoleOutput.d.ts.map +1 -0
  64. package/dist/definitions/outputs/consoleOutput.d.ts +3 -0
  65. package/dist/definitions/outputs/consoleOutput.d.ts.map +1 -0
  66. package/dist/index-browser.es.js +936 -46
  67. package/dist/index-browser.es.js.map +1 -1
  68. package/dist/index-node20.mjs +1023 -0
  69. package/dist/index-node20.mjs.map +1 -0
  70. package/package.json +31 -37
  71. package/src/config.ts +12 -12
  72. package/src/debug/debug.test.ts +50 -0
  73. package/src/debug/debug.ts +100 -0
  74. package/src/formatter-utils/formatObject.test.ts +153 -0
  75. package/src/formatter-utils/formatObject.ts +462 -0
  76. package/src/formatter-utils/formatRecordToString.ts +67 -0
  77. package/src/formatter-utils/index.test.ts +33 -0
  78. package/src/formatter-utils/index.ts +20 -0
  79. package/src/formatter-utils/levelToStyles.ts +14 -0
  80. package/src/formatter-utils/levelToSymbol.ts +14 -0
  81. package/src/formatter-utils/styleToHexColor.ts +9 -0
  82. package/src/formatter-utils/styleToHtmlStyle.ts +69 -0
  83. package/src/formatters/ANSIFormatter.test.ts +27 -0
  84. package/src/formatters/ANSIFormatter.ts +68 -0
  85. package/src/formatters/BrowserConsoleFormatter.test.ts +59 -0
  86. package/src/formatters/BrowserConsoleFormatter.ts +45 -0
  87. package/src/formatters/HTMLFormatter.test.ts +23 -0
  88. package/src/formatters/HTMLFormatter.ts +28 -0
  89. package/src/formatters/JSONFormatter.test.ts +62 -0
  90. package/src/formatters/JSONFormatter.ts +62 -0
  91. package/src/formatters/MarkdownFormatter.test.ts +19 -0
  92. package/src/formatters/MarkdownFormatter.ts +31 -0
  93. package/src/formatters/RawFormatter.test.ts +21 -0
  94. package/src/formatters/RawFormatter.ts +13 -0
  95. package/src/handlers/BrowserConsoleHandler.ts +78 -0
  96. package/src/handlers/ConsoleCLIHandler.ts +41 -0
  97. package/src/handlers/ConsoleHandler.ts +55 -0
  98. package/src/handlers/StringHandler.ts +21 -0
  99. package/src/index.test.ts +29 -29
  100. package/src/index.ts +24 -10
  101. package/src/loggers/LoggerCLI.ts +91 -0
  102. package/src/outputs/cliConsoleOutput.ts +17 -0
  103. package/src/outputs/consoleOutput.ts +18 -0
  104. package/dist/index-browsermodern.es.js +0 -120
  105. package/dist/index-browsermodern.es.js.map +0 -1
  106. package/dist/index-node18.mjs +0 -120
  107. package/dist/index-node18.mjs.map +0 -1
  108. package/src/.eslintrc.json +0 -30
@@ -0,0 +1,20 @@
1
+ import type { LogRecord, Metadata } from "nightingale-types";
2
+
3
+ export { levelToStyles } from "./levelToStyles";
4
+ export { levelToSymbol } from "./levelToSymbol";
5
+ export {
6
+ styleToHtmlStyleThemeDark,
7
+ styleToHtmlStyleThemeLight,
8
+ } from "./styleToHtmlStyle";
9
+ export type { StyleToHtmlStyle } from "./styleToHtmlStyle";
10
+ export { styleToHexColor } from "./styleToHexColor";
11
+ export { formatObject } from "./formatObject";
12
+ export { formatRecordToString } from "./formatRecordToString";
13
+
14
+ export interface NightingaleFormatter {
15
+ format: <T extends Metadata>(record: LogRecord<T>) => string;
16
+ }
17
+
18
+ export interface StringArrayNightingaleFormatter {
19
+ format: <T extends Metadata>(record: LogRecord<T>) => string[];
20
+ }
@@ -0,0 +1,14 @@
1
+ import { Level } from "nightingale-levels";
2
+
3
+ export type LevelToStyles = Readonly<Record<number, string[]>>;
4
+
5
+ export const levelToStyles: LevelToStyles = {
6
+ [Level.TRACE]: ["gray"],
7
+ [Level.DEBUG]: ["gray"],
8
+ // [Level.INFO]: ['gray'],
9
+ [Level.WARN]: ["yellow"],
10
+ [Level.ERROR]: ["red", "bold"],
11
+ [Level.CRITICAL]: ["red", "bold"],
12
+ [Level.FATAL]: ["bgRed", "white"],
13
+ [Level.EMERGENCY]: ["bgRed", "white"],
14
+ };
@@ -0,0 +1,14 @@
1
+ import { Level } from "nightingale-levels";
2
+
3
+ export type LevelToSymbol = Readonly<Record<number, string>>;
4
+
5
+ export const levelToSymbol: LevelToSymbol = {
6
+ [Level.TRACE]: "•",
7
+ [Level.DEBUG]: "•",
8
+ [Level.INFO]: "→",
9
+ [Level.WARN]: "⚠",
10
+ [Level.ERROR]: "✖",
11
+ [Level.CRITICAL]: "!",
12
+ [Level.FATAL]: "‼",
13
+ [Level.EMERGENCY]: "‼",
14
+ };
@@ -0,0 +1,9 @@
1
+ export const styleToHexColor = {
2
+ orange: "ff5f00",
3
+ grayLight: "808080",
4
+ "gray-light": "808080",
5
+ } as const;
6
+
7
+ export type StyleToHexColor = Readonly<
8
+ Record<keyof typeof styleToHexColor, string>
9
+ >;
@@ -0,0 +1,69 @@
1
+ import { styleToHexColor } from "./styleToHexColor";
2
+
3
+ export interface HtmlStyle {
4
+ readonly open: string;
5
+ readonly close: string;
6
+ }
7
+
8
+ export const styleToHtmlStyleThemeLight = {
9
+ // text style
10
+ bold: { open: "font-weight: bold", close: "font-weight: normal" },
11
+ italic: { open: "font-style: italic", close: "font-style: normal" },
12
+ underline: {
13
+ open: "text-decoration: underline",
14
+ close: "text-decoration: none",
15
+ },
16
+ inverse: {
17
+ open: "unicode-bidi: bidi-override; direction: rtl",
18
+ close: "unicode-bidi: normal; direction: ltr",
19
+ },
20
+ strikethrough: {
21
+ open: "text-decoration: line-through",
22
+ close: "text-decoration: none",
23
+ },
24
+
25
+ black: { open: "color: black", close: "color: currentcolor" },
26
+ red: { open: "color: #ff0020", close: "color: currentcolor" },
27
+ green: { open: "color: #00b317", close: "color: currentcolor" },
28
+ yellow: { open: "color: #ffcc00", close: "color: currentcolor" },
29
+ blue: { open: "color: #00a0ff", close: "color: currentcolor" },
30
+ magenta: { open: "color: #ff00a0", close: "color: currentcolor" },
31
+ cyan: { open: "color: #00cfd8", close: "color: currentcolor" },
32
+ white: { open: "color: white", close: "color: currentcolor" },
33
+ gray: { open: "color: gray", close: "color: currentcolor" },
34
+
35
+ bgBlack: { open: "background: black", close: "background: initial" },
36
+ bgRed: { open: "background: #ff0020", close: "background: initial" },
37
+ bgGreen: { open: "background: #00b317", close: "background: initial" },
38
+ bgYellow: { open: "background: #ffcc00", close: "background: initial" },
39
+ bgBlue: { open: "background: #00a0ff", close: "background: initial" },
40
+ bgMagenta: { open: "background: #ff00a0", close: "background: initial" },
41
+ bgCyan: { open: "background: #00cfd8", close: "background: initial" },
42
+ bgWhite: { open: "background: white", close: "background: initial" },
43
+
44
+ orange: {
45
+ open: `color: #${styleToHexColor.orange}`,
46
+ close: "color: currentcolor",
47
+ },
48
+ grayLight: {
49
+ open: `color: #${styleToHexColor.grayLight}`,
50
+ close: "color: currentcolor",
51
+ },
52
+ "gray-light": {
53
+ open: `color: #${styleToHexColor.grayLight}`,
54
+ close: "color: currentcolor",
55
+ },
56
+ } as const;
57
+
58
+ export type StyleToHtmlStyle = Readonly<
59
+ Record<keyof typeof styleToHtmlStyleThemeLight, HtmlStyle>
60
+ >;
61
+
62
+ export const styleToHtmlStyleThemeDark: StyleToHtmlStyle = {
63
+ ...styleToHtmlStyleThemeLight,
64
+ black: styleToHtmlStyleThemeLight.white,
65
+ bgBlack: styleToHtmlStyleThemeLight.bgWhite,
66
+ white: styleToHtmlStyleThemeLight.black,
67
+ bgWhite: styleToHtmlStyleThemeLight.bgBlack,
68
+ gray: { open: "color: lightgray", close: "color: currentcolor" },
69
+ };
@@ -0,0 +1,27 @@
1
+ /* eslint-disable no-control-regex */
2
+ import { ANSIFormatter, style } from "./ANSIFormatter";
3
+
4
+ test("style: blue bold color", () => {
5
+ expect(style(["blue", "bold"], "test").replace(/\u001B/g, "ESC")).toBe(
6
+ "ESC[1mESC[34mtestESC[39mESC[22m",
7
+ );
8
+ });
9
+
10
+ test("style: ansi256 color", () => {
11
+ expect(style(["orange"], "test").replace(/\u001B/g, "ESC")).toBe(
12
+ "ESC[38;5;208mtestESC[39m",
13
+ );
14
+ });
15
+
16
+ test("format simple message", () => {
17
+ expect(
18
+ ANSIFormatter.format({
19
+ key: "test",
20
+ level: 100,
21
+ message: "test",
22
+ datetime: new Date(2000, 1, 1, 1, 1, 1),
23
+ }).replace(/\u001B/g, "ESC"),
24
+ ).toBe(
25
+ "ESC[38;5;244mtestESC[39m ESC[1mESC[90m01:01:01ESC[39mESC[22m ESC[90m• testESC[39m",
26
+ );
27
+ });
@@ -0,0 +1,68 @@
1
+ import ansi from "ansi-styles";
2
+ import type { NightingaleFormatter } from "nightingale";
3
+ import type { Styles } from "nightingale-types";
4
+ import { formatRecordToString, styleToHexColor } from "../formatter-utils";
5
+
6
+ export type { Styles } from "nightingale-types";
7
+
8
+ interface CodePair {
9
+ open: string;
10
+ close: string;
11
+ }
12
+
13
+ type AnsiStyles = Record<string, CodePair | undefined>;
14
+
15
+ const ansiStyles: AnsiStyles = {
16
+ black: ansi.black,
17
+ red: ansi.red,
18
+ green: ansi.green,
19
+ yellow: ansi.yellow,
20
+ blue: ansi.blue,
21
+ magenta: ansi.magenta,
22
+ cyan: ansi.cyan,
23
+ white: ansi.white,
24
+ gray: ansi.gray,
25
+
26
+ bgBlack: ansi.bgBlack,
27
+ bgRed: ansi.bgRed,
28
+ bgGreen: ansi.bgGreen,
29
+ bgYellow: ansi.bgYellow,
30
+ bgBlue: ansi.bgBlue,
31
+ bgMagenta: ansi.bgMagenta,
32
+ bgCyan: ansi.bgCyan,
33
+ bgWhite: ansi.bgWhite,
34
+
35
+ bold: ansi.bold,
36
+ underline: ansi.underline,
37
+
38
+ // http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html
39
+ orange: {
40
+ open: ansi.color.ansi256(ansi.hexToAnsi256(styleToHexColor.orange)),
41
+ close: ansi.color.close,
42
+ },
43
+ "gray-light": {
44
+ open: ansi.color.ansi256(ansi.hexToAnsi256(styleToHexColor["gray-light"])),
45
+ close: ansi.color.close,
46
+ },
47
+ };
48
+
49
+ export function style(styles: Styles, string: string): string {
50
+ if (!styles || styles.length === 0 || !string) {
51
+ return string;
52
+ }
53
+
54
+ // eslint-disable-next-line unicorn/no-array-reduce
55
+ return styles.reduce((styledString: string, styleName: string) => {
56
+ const codePair: CodePair | undefined = ansiStyles[styleName];
57
+
58
+ if (!codePair) {
59
+ throw new Error(`Unknown style: ${styleName}`);
60
+ }
61
+
62
+ return codePair.open + styledString + codePair.close;
63
+ }, string);
64
+ }
65
+
66
+ export const ANSIFormatter: NightingaleFormatter = {
67
+ format: (record) => formatRecordToString(record, style),
68
+ };
@@ -0,0 +1,59 @@
1
+ import type { StyleToHtmlStyle } from "nightingale";
2
+ import { Level } from "nightingale-levels";
3
+ import { styleToHtmlStyleThemeLight } from "../formatter-utils";
4
+ import { BrowserConsoleFormatter, style } from "./BrowserConsoleFormatter";
5
+
6
+ const styleToHtmlStyle: StyleToHtmlStyle = styleToHtmlStyleThemeLight;
7
+ const formatterWithLightTheme = new BrowserConsoleFormatter("light");
8
+ const formatterWithDarkTheme = new BrowserConsoleFormatter("dark");
9
+
10
+ test("style: blue bold color", () => {
11
+ const args: string[] = [];
12
+ expect(style(styleToHtmlStyle, args)(["blue", "bold"], "test")).toBe(
13
+ "%ctest%c",
14
+ );
15
+ expect(args).toEqual([
16
+ "color: #00a0ff; font-weight: bold",
17
+ "color: currentcolor; font-weight: normal",
18
+ ]);
19
+ });
20
+
21
+ test("format simple message, with light theme", () => {
22
+ const record = {
23
+ key: "record.key",
24
+ level: Level.INFO,
25
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
26
+ message: "test",
27
+ metadata: {},
28
+ extra: {},
29
+ };
30
+
31
+ const [string, ...args] = formatterWithLightTheme.format(record);
32
+ expect(string).toBe("%crecord.key%c %c01:00:00%c → test");
33
+ expect(args).toEqual([
34
+ "color: #808080",
35
+ "color: currentcolor",
36
+ "color: gray; font-weight: bold",
37
+ "color: currentcolor; font-weight: normal",
38
+ ]);
39
+ });
40
+
41
+ test("format simple message, with dark theme", () => {
42
+ const record = {
43
+ key: "record.key",
44
+ level: Level.INFO,
45
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
46
+ message: "test",
47
+ metadata: {},
48
+ extra: {},
49
+ };
50
+
51
+ const [string, ...args] = formatterWithDarkTheme.format(record);
52
+ expect(string).toBe("%crecord.key%c %c01:00:00%c → test");
53
+ expect(args).toEqual([
54
+ "color: #808080",
55
+ "color: currentcolor",
56
+ "color: lightgray; font-weight: bold",
57
+ "color: currentcolor; font-weight: normal",
58
+ ]);
59
+ });
@@ -0,0 +1,45 @@
1
+ import type { StyleToHtmlStyle } from "nightingale";
2
+ import type { LogRecord, Metadata, Styles } from "nightingale-types";
3
+ import {
4
+ formatRecordToString,
5
+ styleToHtmlStyleThemeDark,
6
+ styleToHtmlStyleThemeLight,
7
+ } from "../formatter-utils";
8
+ import type { StringArrayNightingaleFormatter } from "../formatter-utils";
9
+
10
+ export const style =
11
+ (styleToHtmlStyle: StyleToHtmlStyle, args: string[]) =>
12
+ (styles: Styles, string: string): string => {
13
+ if (!styles || styles.length === 0 || !string) {
14
+ return string;
15
+ }
16
+
17
+ const htmlStyles = styles.map(
18
+ (styleName) => styleToHtmlStyle[styleName as keyof StyleToHtmlStyle],
19
+ );
20
+
21
+ args.push(
22
+ htmlStyles.map((s) => s.open).join("; "),
23
+ htmlStyles.map((s) => s.close).join("; "),
24
+ );
25
+ return `%c${string}%c`;
26
+ };
27
+
28
+ export class BrowserConsoleFormatter
29
+ implements StringArrayNightingaleFormatter
30
+ {
31
+ styleToHtmlStyle: StyleToHtmlStyle;
32
+ constructor(theme: "dark" | "light" = "light") {
33
+ this.styleToHtmlStyle =
34
+ theme === "dark" ? styleToHtmlStyleThemeDark : styleToHtmlStyleThemeLight;
35
+ }
36
+
37
+ format<T extends Metadata>(record: LogRecord<T>): string[] {
38
+ const args: string[] = [];
39
+ const string = formatRecordToString(
40
+ record,
41
+ style(this.styleToHtmlStyle, args),
42
+ );
43
+ return [string, ...args];
44
+ }
45
+ }
@@ -0,0 +1,23 @@
1
+ import { Level } from "nightingale-levels";
2
+ import { HTMLFormatter, style } from "./HTMLFormatter";
3
+
4
+ test("blue bold color", () => {
5
+ expect(style(["blue", "bold"], "test")).toBe(
6
+ '<span style="color: #00a0ff; font-weight: bold">test</span>',
7
+ );
8
+ });
9
+
10
+ test("format simple message", () => {
11
+ const record = {
12
+ key: "record.key",
13
+ level: Level.INFO,
14
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
15
+ message: "record.message",
16
+ metadata: {},
17
+ extra: {},
18
+ };
19
+
20
+ expect(HTMLFormatter.format(record)).toBe(
21
+ '<span style="color: #808080">record.key</span> <span style="color: gray; font-weight: bold">01:00:00</span> → record.message',
22
+ );
23
+ });
@@ -0,0 +1,28 @@
1
+ import type { Styles } from "nightingale-types";
2
+ import type {
3
+ NightingaleFormatter,
4
+ StyleToHtmlStyle,
5
+ } from "../formatter-utils";
6
+ import {
7
+ formatRecordToString,
8
+ styleToHtmlStyleThemeLight,
9
+ } from "../formatter-utils";
10
+
11
+ export function style(styles: Styles, string: string): string {
12
+ if (!styles || styles.length === 0 || !string) {
13
+ return string;
14
+ }
15
+
16
+ return `<span style="${styles
17
+ .map(
18
+ (styleName) =>
19
+ styleToHtmlStyleThemeLight[styleName as keyof StyleToHtmlStyle].open,
20
+ )
21
+ .join("; ")}">${string}</span>`;
22
+ }
23
+
24
+ export const HTMLFormatter: NightingaleFormatter = {
25
+ format(record) {
26
+ return formatRecordToString(record, style);
27
+ },
28
+ };
@@ -0,0 +1,62 @@
1
+ import { Level } from "nightingale-levels";
2
+ import { JSONFormatter } from "./JSONFormatter";
3
+
4
+ test("format record", () => {
5
+ const record = {
6
+ key: "record.key",
7
+ level: Level.INFO,
8
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
9
+ message: "record.message",
10
+ metadata: {},
11
+ extra: {},
12
+ };
13
+
14
+ expect(JSONFormatter.format(record)).toBe(JSON.stringify(record));
15
+ });
16
+
17
+ test("format error", () => {
18
+ const error = new Error("test message");
19
+ const record = {
20
+ key: "record.key",
21
+ level: Level.INFO,
22
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
23
+ message: "record.message",
24
+ metadata: {
25
+ error,
26
+ },
27
+ extra: {},
28
+ };
29
+
30
+ expect(JSONFormatter.format(record)).toBe(
31
+ JSON.stringify({
32
+ ...record,
33
+ metadata: { error: { message: "test message", stack: error.stack } },
34
+ }),
35
+ );
36
+ });
37
+
38
+ test("format map", () => {
39
+ const map = new Map<unknown, unknown>([
40
+ [1, "value1"],
41
+ ["2", "value2"],
42
+ ["3", 3],
43
+ [{}, "ignore value"],
44
+ ]);
45
+ const record = {
46
+ key: "record.key",
47
+ level: Level.INFO,
48
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
49
+ message: "record.message",
50
+ metadata: {
51
+ map,
52
+ },
53
+ extra: {},
54
+ };
55
+
56
+ expect(JSONFormatter.format(record)).toBe(
57
+ JSON.stringify({
58
+ ...record,
59
+ metadata: { map: { "1": "value1", "2": "value2", "3": 3 } },
60
+ }),
61
+ );
62
+ });
@@ -0,0 +1,62 @@
1
+ import type { NightingaleFormatter } from "../formatter-utils";
2
+
3
+ function map2object(map: Map<unknown, unknown>): unknown {
4
+ const object: Record<string, unknown> = {};
5
+
6
+ map.forEach((value, key) => {
7
+ if (typeof key === "object") {
8
+ // ignore key
9
+ return;
10
+ }
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
13
+ object[String(key)] = value;
14
+ });
15
+
16
+ return object;
17
+ }
18
+
19
+ function stringify(value: unknown, space?: number | string): string {
20
+ return JSON.stringify(
21
+ value,
22
+ (key, objectValue) => {
23
+ if (objectValue instanceof Map) {
24
+ return map2object(objectValue);
25
+ }
26
+ if (objectValue instanceof Error) {
27
+ return {
28
+ message: objectValue.message,
29
+ stack: objectValue.stack,
30
+ };
31
+ }
32
+
33
+ return objectValue as unknown;
34
+ },
35
+ space,
36
+ );
37
+ }
38
+
39
+ export const JSONFormatter: NightingaleFormatter = {
40
+ format(record) {
41
+ return stringify({
42
+ key: record.key,
43
+ level: record.level,
44
+ datetime: record.datetime,
45
+ message: record.message,
46
+ metadata: record.metadata,
47
+ extra: record.extra,
48
+ });
49
+ },
50
+ };
51
+
52
+ export const JSONCLIFormatter: NightingaleFormatter = {
53
+ format(record) {
54
+ return stringify({
55
+ key: record.key,
56
+ time: record.datetime.toTimeString().split(" ", 2)[0]!,
57
+ message: record.message,
58
+ ...record.metadata,
59
+ ...record.extra,
60
+ });
61
+ },
62
+ };
@@ -0,0 +1,19 @@
1
+ import { Level } from "nightingale-levels";
2
+ import { MarkdownFormatter, style } from "./MarkdownFormatter";
3
+
4
+ test("style: blue bold color", () => {
5
+ expect(style(["blue", "bold"], "test")).toBe("*test*");
6
+ });
7
+
8
+ test("format simple message", () => {
9
+ const record = {
10
+ key: "record.key",
11
+ level: Level.INFO,
12
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
13
+ message: "test",
14
+ metadata: {},
15
+ extra: {},
16
+ };
17
+
18
+ expect(MarkdownFormatter.format(record)).toBe("record.key *01:00:00* → test");
19
+ });
@@ -0,0 +1,31 @@
1
+ import type { Styles } from "nightingale-types";
2
+ import type { NightingaleFormatter } from "../formatter-utils";
3
+ import { formatRecordToString } from "../formatter-utils";
4
+
5
+ export function style(styles: Styles, string: string): string {
6
+ if (!styles || styles.length === 0 || !string) {
7
+ return string;
8
+ }
9
+
10
+ // eslint-disable-next-line unicorn/no-array-reduce
11
+ return styles.reduce((part, styleName) => {
12
+ switch (styleName) {
13
+ case "bold":
14
+ return `*${part}*`;
15
+ case "italic":
16
+ return `_${part}_`;
17
+ case "strikethrough":
18
+ return `~${part}~`;
19
+
20
+ // no default
21
+ }
22
+
23
+ return part;
24
+ }, string);
25
+ }
26
+
27
+ export const MarkdownFormatter: NightingaleFormatter = {
28
+ format(record) {
29
+ return formatRecordToString(record, style);
30
+ },
31
+ };
@@ -0,0 +1,21 @@
1
+ import { Level } from "nightingale-levels";
2
+ import { RawFormatter, style } from "./RawFormatter";
3
+
4
+ test("style: blue bold color", () => {
5
+ expect(style(["blue", "bold"], "test")).toBe("test");
6
+ });
7
+
8
+ test("format simple message", () => {
9
+ const record = {
10
+ key: "record.key",
11
+ level: Level.INFO,
12
+ datetime: new Date(2000, 1, 1, 1, 0, 0),
13
+ message: "record.message",
14
+ metadata: {},
15
+ extra: {},
16
+ };
17
+
18
+ expect(RawFormatter.format(record)).toBe(
19
+ "record.key 01:00:00 → record.message",
20
+ );
21
+ });
@@ -0,0 +1,13 @@
1
+ import type { Styles } from "nightingale-types";
2
+ import type { NightingaleFormatter } from "../formatter-utils";
3
+ import { formatRecordToString } from "../formatter-utils";
4
+
5
+ export function style(styles: Styles, value: string): string {
6
+ return value;
7
+ }
8
+
9
+ export const RawFormatter: NightingaleFormatter = {
10
+ format(record) {
11
+ return formatRecordToString(record, style);
12
+ },
13
+ };
@@ -0,0 +1,78 @@
1
+ import type {
2
+ Handle,
3
+ Handler,
4
+ IsHandling,
5
+ Level,
6
+ LogRecord,
7
+ Metadata,
8
+ } from "nightingale-types";
9
+ import { createFindDebugLevel } from "../debug/debug";
10
+ import { BrowserConsoleFormatter } from "../formatters/BrowserConsoleFormatter";
11
+ import { consoleOutput } from "../outputs/consoleOutput";
12
+
13
+ export function getDebugString(): string {
14
+ const querystring = document.location.search;
15
+ const debugFromLocalStorage =
16
+ // eslint-disable-next-line unicorn/prefer-global-this, @typescript-eslint/no-unnecessary-condition
17
+ window.localStorage?.getItem("debug") || "";
18
+
19
+ if (!querystring) {
20
+ return debugFromLocalStorage;
21
+ }
22
+
23
+ // https://developer.mozilla.org/en-US/docs/Web/API/URLUtils/search#Get_the_value_of_a_single_search_param
24
+ const debugFromQueryString = decodeURI(
25
+ querystring.replace(
26
+ // eslint-disable-next-line prefer-regex-literals, regexp/no-super-linear-backtracking
27
+ new RegExp("^(?:.*[&?]DEBUG(?:=([^&]*))?)?.*$", "i"),
28
+ "$1",
29
+ ),
30
+ );
31
+
32
+ return (
33
+ (debugFromLocalStorage ? `${debugFromLocalStorage},` : "") +
34
+ debugFromQueryString
35
+ );
36
+ }
37
+
38
+ // debug string can change any time (localStorage), so we need a new object each time.
39
+ const findDebugLevel = (minLevel: Level, key: string): Level =>
40
+ createFindDebugLevel(getDebugString())(minLevel, key);
41
+
42
+ type Theme = "dark" | "light";
43
+
44
+ const getDefaultTheme = (): Theme => {
45
+ try {
46
+ const configInLocalStorage = localStorage.getItem("NIGHTINGALE_THEME");
47
+ if (configInLocalStorage && configInLocalStorage === "dark") {
48
+ return configInLocalStorage;
49
+ }
50
+ } catch {}
51
+ return "light";
52
+ };
53
+
54
+ const createHandler = (theme: Theme = getDefaultTheme()): Handle => {
55
+ const browserConsoleFormatter = new BrowserConsoleFormatter(theme);
56
+ return <T extends Metadata>(record: LogRecord<T>) => {
57
+ consoleOutput(browserConsoleFormatter.format(record), record);
58
+ };
59
+ };
60
+
61
+ export interface BrowserConsoleHandlerOptions {
62
+ theme?: Theme;
63
+ }
64
+
65
+ export class BrowserConsoleHandler implements Handler {
66
+ minLevel: Level = 0;
67
+
68
+ handle: Handle;
69
+
70
+ isHandling: IsHandling;
71
+
72
+ constructor(minLevel: Level, options: BrowserConsoleHandlerOptions = {}) {
73
+ this.isHandling = (level: Level, key: string) =>
74
+ level >= findDebugLevel(minLevel, key);
75
+
76
+ this.handle = createHandler(options.theme);
77
+ }
78
+ }