lognal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/core/filter.d.ts +29 -0
  4. package/dist/core/filter.js +82 -0
  5. package/dist/core/layout/entry-lines.d.ts +16 -0
  6. package/dist/core/layout/entry-lines.js +120 -0
  7. package/dist/core/layout/layout.d.ts +152 -0
  8. package/dist/core/layout/layout.js +784 -0
  9. package/dist/core/layout/row-index.d.ts +33 -0
  10. package/dist/core/layout/row-index.js +99 -0
  11. package/dist/core/layout/types.d.ts +128 -0
  12. package/dist/core/layout/types.js +8 -0
  13. package/dist/core/store.d.ts +96 -0
  14. package/dist/core/store.js +204 -0
  15. package/dist/core/text/ansi.d.ts +20 -0
  16. package/dist/core/text/ansi.js +187 -0
  17. package/dist/core/text/graphemes.d.ts +17 -0
  18. package/dist/core/text/graphemes.js +70 -0
  19. package/dist/core/text/line-splitter.d.ts +18 -0
  20. package/dist/core/text/line-splitter.js +56 -0
  21. package/dist/core/text/measure.d.ts +10 -0
  22. package/dist/core/text/measure.js +38 -0
  23. package/dist/core/text/shape.d.ts +24 -0
  24. package/dist/core/text/shape.js +220 -0
  25. package/dist/core/text/unicode-width-data.d.ts +54 -0
  26. package/dist/core/text/unicode-width-data.js +227 -0
  27. package/dist/core/text/width.d.ts +20 -0
  28. package/dist/core/text/width.js +157 -0
  29. package/dist/core/text/wrap.d.ts +14 -0
  30. package/dist/core/text/wrap.js +94 -0
  31. package/dist/core/time.d.ts +10 -0
  32. package/dist/core/time.js +24 -0
  33. package/dist/core/types.d.ts +131 -0
  34. package/dist/core/types.js +9 -0
  35. package/dist/core/value/preview.d.ts +16 -0
  36. package/dist/core/value/preview.js +207 -0
  37. package/dist/index.d.ts +27 -0
  38. package/dist/index.js +24 -0
  39. package/dist/lognal.css +589 -0
  40. package/dist/react/LogViewer.d.ts +32 -0
  41. package/dist/react/LogViewer.js +183 -0
  42. package/dist/react/index.d.ts +1 -0
  43. package/dist/react/index.js +2 -0
  44. package/dist/renderer/canvas/canvas-renderer.d.ts +45 -0
  45. package/dist/renderer/canvas/canvas-renderer.js +464 -0
  46. package/dist/renderer/canvas/palette.d.ts +6 -0
  47. package/dist/renderer/canvas/palette.js +25 -0
  48. package/dist/renderer/theme.d.ts +3 -0
  49. package/dist/renderer/theme.js +52 -0
  50. package/dist/renderer/types.d.ts +82 -0
  51. package/dist/renderer/types.js +1 -0
  52. package/dist/sources/console/format.d.ts +32 -0
  53. package/dist/sources/console/format.js +189 -0
  54. package/dist/sources/console/hook.d.ts +27 -0
  55. package/dist/sources/console/hook.js +94 -0
  56. package/dist/sources/console/recorder.d.ts +41 -0
  57. package/dist/sources/console/recorder.js +239 -0
  58. package/dist/sources/console/snapshot.d.ts +22 -0
  59. package/dist/sources/console/snapshot.js +547 -0
  60. package/dist/sources/console/table.d.ts +9 -0
  61. package/dist/sources/console/table.js +87 -0
  62. package/dist/sources/text/encoding.d.ts +11 -0
  63. package/dist/sources/text/encoding.js +59 -0
  64. package/dist/sources/text/follow-file.d.ts +49 -0
  65. package/dist/sources/text/follow-file.js +109 -0
  66. package/dist/sources/text/read-file.d.ts +63 -0
  67. package/dist/sources/text/read-file.js +82 -0
  68. package/dist/viewer/icons.d.ts +14 -0
  69. package/dist/viewer/icons.js +30 -0
  70. package/dist/viewer/input-line.d.ts +46 -0
  71. package/dist/viewer/input-line.js +144 -0
  72. package/dist/viewer/labels.d.ts +35 -0
  73. package/dist/viewer/labels.js +61 -0
  74. package/dist/viewer/scrollbar.d.ts +27 -0
  75. package/dist/viewer/scrollbar.js +143 -0
  76. package/dist/viewer/theme.d.ts +15 -0
  77. package/dist/viewer/theme.js +105 -0
  78. package/dist/viewer/viewer.d.ts +217 -0
  79. package/dist/viewer/viewer.js +1201 -0
  80. package/package.json +92 -0
@@ -0,0 +1,6 @@
1
+ import type { TextColor } from '../../core/types.js';
2
+ /**
3
+ * Returns the CSS color for an ANSI color. Indexes 0 to 15 come from the theme, 16 to 231 are
4
+ * the 6×6×6 color cube, and 232 to 255 are the gray ramp. Strings are used as they are.
5
+ */
6
+ export declare const resolveAnsiColor: (color: TextColor, themeColors: readonly string[]) => string;
@@ -0,0 +1,25 @@
1
+ const CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
2
+ const toHex = (value) => {
3
+ return value.toString(16).padStart(2, '0');
4
+ };
5
+ /**
6
+ * Returns the CSS color for an ANSI color. Indexes 0 to 15 come from the theme, 16 to 231 are
7
+ * the 6×6×6 color cube, and 232 to 255 are the gray ramp. Strings are used as they are.
8
+ */
9
+ export const resolveAnsiColor = (color, themeColors) => {
10
+ if (typeof color === 'string') {
11
+ return color;
12
+ }
13
+ if (color < 16) {
14
+ return themeColors[color] ?? themeColors[7] ?? '#ffffff';
15
+ }
16
+ if (color < 232) {
17
+ const index = color - 16;
18
+ const red = CUBE_LEVELS[Math.floor(index / 36) % 6];
19
+ const green = CUBE_LEVELS[Math.floor(index / 6) % 6];
20
+ const blue = CUBE_LEVELS[index % 6];
21
+ return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
22
+ }
23
+ const gray = 8 + (Math.min(color, 255) - 232) * 10;
24
+ return `#${toHex(gray)}${toHex(gray)}${toHex(gray)}`;
25
+ };
@@ -0,0 +1,3 @@
1
+ import type { RenderTheme } from './types.js';
2
+ /** The colors used until a theme is read from CSS: the dark palette of `lognal.css`. */
3
+ export declare const DEFAULT_RENDER_THEME: RenderTheme;
@@ -0,0 +1,52 @@
1
+ /** The colors used until a theme is read from CSS: the dark palette of `lognal.css`. */
2
+ export const DEFAULT_RENDER_THEME = {
3
+ background: '#16181d',
4
+ foreground: '#e3e5ea',
5
+ muted: '#8f94a1',
6
+ accent: '#5aa2ff',
7
+ selection: 'rgba(90, 162, 255, 0.3)',
8
+ match: 'rgba(252, 191, 50, 0.3)',
9
+ separator: 'rgba(227, 229, 234, 0.05)',
10
+ error: '#ff8a8d',
11
+ errorBackground: 'rgba(252, 79, 83, 0.1)',
12
+ warn: '#fcc549',
13
+ warnBackground: 'rgba(252, 191, 50, 0.08)',
14
+ info: '#5aa2ff',
15
+ debug: '#8f94a1',
16
+ tokens: {
17
+ muted: '#8f94a1',
18
+ string: '#7fd6a4',
19
+ number: '#b9a8ff',
20
+ boolean: '#b9a8ff',
21
+ null: '#8f94a1',
22
+ key: '#82bdff',
23
+ symbol: '#f5a3d7',
24
+ function: '#82bdff',
25
+ regexp: '#ffa585',
26
+ date: '#7fd6a4',
27
+ tag: '#82bdff',
28
+ attribute: '#fcc549',
29
+ error: '#ff8a8d',
30
+ warn: '#fcc549',
31
+ info: '#5aa2ff',
32
+ accent: '#5aa2ff'
33
+ },
34
+ ansi: [
35
+ '#3b3f4a',
36
+ '#fc5c60',
37
+ '#43d786',
38
+ '#fcbf32',
39
+ '#5aa2ff',
40
+ '#c792ea',
41
+ '#56d4dd',
42
+ '#d0d3db',
43
+ '#6b7080',
44
+ '#ff8a8d',
45
+ '#7ee3a8',
46
+ '#ffd466',
47
+ '#82bdff',
48
+ '#ddb6f2',
49
+ '#8ae6ec',
50
+ '#ffffff'
51
+ ]
52
+ };
@@ -0,0 +1,82 @@
1
+ import type { VisualRow } from '../core/layout/types.js';
2
+ import type { StyleToken } from '../core/types.js';
3
+ /** The font the log text is drawn with. Only monospace fonts line up on the grid. */
4
+ export interface FontSettings {
5
+ /** A CSS font family list, such as `"JetBrains Mono", D2Coding, monospace`. */
6
+ family: string;
7
+ /** Font size in CSS pixels. */
8
+ size: number;
9
+ /** Font weight of regular text, such as `400`. */
10
+ weight: number;
11
+ /** Row height as a multiple of the font size. */
12
+ lineHeight: number;
13
+ }
14
+ /** The size of one cell of the text grid, in CSS pixels. */
15
+ export interface CellMetrics {
16
+ width: number;
17
+ height: number;
18
+ /** Distance from the top of a row to the text baseline. */
19
+ baseline: number;
20
+ }
21
+ /** Colors the renderer draws with. Any CSS color works. */
22
+ export interface RenderTheme {
23
+ background: string;
24
+ foreground: string;
25
+ muted: string;
26
+ accent: string;
27
+ selection: string;
28
+ match: string;
29
+ separator: string;
30
+ error: string;
31
+ errorBackground: string;
32
+ warn: string;
33
+ warnBackground: string;
34
+ info: string;
35
+ debug: string;
36
+ tokens: Record<Exclude<StyleToken, 'default'>, string>;
37
+ /** The 16 ANSI colors: black, red, green, yellow, blue, magenta, cyan, white, then bright. */
38
+ ansi: string[];
39
+ }
40
+ /** Highlights on one row, as ranges of columns of the content area. */
41
+ export interface RowDecoration {
42
+ selection?: [number, number];
43
+ matches?: [number, number][];
44
+ }
45
+ /** Everything needed to draw one frame. */
46
+ export interface RenderFrame {
47
+ rows: VisualRow[];
48
+ decorations: RowDecoration[];
49
+ /** Vertical offset of the first row in CSS pixels, zero or negative. */
50
+ offsetY: number;
51
+ /** Horizontal scroll of the content area in CSS pixels. */
52
+ scrollX: number;
53
+ /** Space before the gutter in CSS pixels. */
54
+ paddingLeft: number;
55
+ /** Cells taken by the timestamp column, or 0 when timestamps are hidden. */
56
+ timestampCells: number;
57
+ /** Cells taken by the level marker column. */
58
+ markerCells: number;
59
+ /** Formats the time of an entry for the timestamp column. */
60
+ formatTime: (time: number) => string;
61
+ }
62
+ /**
63
+ * Draws frames of the log. The viewer owns the layout, scrolling and input; a renderer only
64
+ * turns a frame into pixels, so a different drawing technology can take its place.
65
+ */
66
+ export interface Renderer {
67
+ /** The element the renderer draws into. */
68
+ readonly element: HTMLElement;
69
+ setTheme(theme: RenderTheme): void;
70
+ /** Sets the font and returns the size of a cell. */
71
+ setFont(font: FontSettings): CellMetrics;
72
+ getMetrics(): CellMetrics;
73
+ /** Sets the drawing size in CSS pixels. */
74
+ resize(width: number, height: number, pixelRatio: number): void;
75
+ render(frame: RenderFrame): void;
76
+ /**
77
+ * Called when fonts finish loading glyphs the last frame needed, so the viewer can measure
78
+ * again and redraw.
79
+ */
80
+ onFontsChanged(listener: () => void): void;
81
+ dispose(): void;
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { LogPart, TextStyle, ValueNode } from '../../core/types.js';
2
+ /** Captures a value for display. */
3
+ export type ValueCapture = (value: unknown) => ValueNode;
4
+ /** Returns whether a CSS color can be used as is. Anything that could load a resource is rejected. */
5
+ export declare const isSafeColor: (value: string) => boolean;
6
+ /**
7
+ * Reads the CSS given to `%c` and keeps only what the viewer can draw: text and background
8
+ * color, weight, style and decoration. Everything else is ignored, including any value that
9
+ * refers to a URL, so a log message cannot make the page load anything.
10
+ */
11
+ export declare const parseConsoleCss: (css: string) => TextStyle | undefined;
12
+ /**
13
+ * Applies the format specifiers in the first argument, following the Formatter operation of
14
+ * the Console Standard, and returns the parts of the message together with the arguments the
15
+ * specifiers did not consume.
16
+ *
17
+ * `%s` converts with `String`, `%d` and `%i` with `parseInt`, `%f` with `parseFloat`, `%o`
18
+ * and `%O` insert the value itself, and `%c` styles the text that follows it. `%%` writes a
19
+ * percent sign. A specifier with no argument left stays in the text as written.
20
+ */
21
+ export declare const applyFormat: (args: readonly unknown[], capture: ValueCapture) => {
22
+ parts: LogPart[];
23
+ rest: unknown[];
24
+ };
25
+ /**
26
+ * Turns the arguments of a console call into the parts of an entry.
27
+ *
28
+ * With more than one argument and a string first, the format specifiers are applied. The
29
+ * remaining arguments follow, separated by spaces: strings as plain text and everything else
30
+ * as a captured value.
31
+ */
32
+ export declare const formatArguments: (args: readonly unknown[], capture: ValueCapture) => LogPart[];
@@ -0,0 +1,189 @@
1
+ const SAFE_COLOR_FUNCTION = /^(rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([\d\s.,%+\-/a-z]*\)$/i;
2
+ const SAFE_COLOR_HEX = /^#[0-9a-f]{3,8}$/i;
3
+ const SAFE_COLOR_NAME = /^[a-z]{3,30}$/i;
4
+ const SPECIFIER = /%([sdifoOc%])/g;
5
+ /** Returns whether a CSS color can be used as is. Anything that could load a resource is rejected. */
6
+ export const isSafeColor = (value) => {
7
+ const color = value.trim();
8
+ return (SAFE_COLOR_HEX.test(color) || SAFE_COLOR_FUNCTION.test(color) || SAFE_COLOR_NAME.test(color));
9
+ };
10
+ /** Splits a CSS value on spaces that are not inside parentheses. */
11
+ const splitCssValue = (value) => {
12
+ const tokens = [];
13
+ let depth = 0;
14
+ let current = '';
15
+ for (const character of value) {
16
+ if (character === '(') {
17
+ depth++;
18
+ }
19
+ else if (character === ')') {
20
+ depth = Math.max(0, depth - 1);
21
+ }
22
+ if (/\s/.test(character) && depth === 0) {
23
+ if (current) {
24
+ tokens.push(current);
25
+ current = '';
26
+ }
27
+ }
28
+ else {
29
+ current += character;
30
+ }
31
+ }
32
+ if (current) {
33
+ tokens.push(current);
34
+ }
35
+ return tokens;
36
+ };
37
+ /**
38
+ * Reads the CSS given to `%c` and keeps only what the viewer can draw: text and background
39
+ * color, weight, style and decoration. Everything else is ignored, including any value that
40
+ * refers to a URL, so a log message cannot make the page load anything.
41
+ */
42
+ export const parseConsoleCss = (css) => {
43
+ const style = {};
44
+ for (const declaration of css.split(';')) {
45
+ const separator = declaration.indexOf(':');
46
+ if (separator < 0) {
47
+ continue;
48
+ }
49
+ const property = declaration.slice(0, separator).trim().toLowerCase();
50
+ const value = declaration.slice(separator + 1).trim();
51
+ if (property === 'color' && isSafeColor(value)) {
52
+ style.color = value;
53
+ }
54
+ else if (property === 'background' || property === 'background-color') {
55
+ const color = splitCssValue(value).find((token) => isSafeColor(token));
56
+ if (color) {
57
+ style.background = color;
58
+ }
59
+ }
60
+ else if (property === 'font-weight') {
61
+ const weight = Number(value);
62
+ if (value === 'bold' || value === 'bolder' || weight >= 600) {
63
+ style.bold = true;
64
+ }
65
+ }
66
+ else if (property === 'font-style' && /^(italic|oblique)/i.test(value)) {
67
+ style.italic = true;
68
+ }
69
+ else if (property === 'text-decoration' || property === 'text-decoration-line') {
70
+ if (/underline/i.test(value)) {
71
+ style.underline = true;
72
+ }
73
+ if (/line-through/i.test(value)) {
74
+ style.strikethrough = true;
75
+ }
76
+ }
77
+ }
78
+ return Object.keys(style).length > 0 ? style : undefined;
79
+ };
80
+ const toText = (value) => {
81
+ if (typeof value === 'string') {
82
+ return value;
83
+ }
84
+ try {
85
+ return String(value);
86
+ }
87
+ catch {
88
+ return Object.prototype.toString.call(value);
89
+ }
90
+ };
91
+ const toInteger = (value) => {
92
+ return typeof value === 'symbol' ? 'NaN' : String(parseInt(toText(value), 10));
93
+ };
94
+ const toFloat = (value) => {
95
+ return typeof value === 'symbol' ? 'NaN' : String(parseFloat(toText(value)));
96
+ };
97
+ /**
98
+ * Applies the format specifiers in the first argument, following the Formatter operation of
99
+ * the Console Standard, and returns the parts of the message together with the arguments the
100
+ * specifiers did not consume.
101
+ *
102
+ * `%s` converts with `String`, `%d` and `%i` with `parseInt`, `%f` with `parseFloat`, `%o`
103
+ * and `%O` insert the value itself, and `%c` styles the text that follows it. `%%` writes a
104
+ * percent sign. A specifier with no argument left stays in the text as written.
105
+ */
106
+ export const applyFormat = (args, capture) => {
107
+ const [format, ...values] = args;
108
+ const parts = [];
109
+ if (typeof format !== 'string') {
110
+ return { parts, rest: [...args] };
111
+ }
112
+ let style;
113
+ let text = '';
114
+ let last = 0;
115
+ let consumed = 0;
116
+ const flush = () => {
117
+ if (text) {
118
+ const part = { type: 'text', text };
119
+ if (style) {
120
+ part.style = style;
121
+ }
122
+ parts.push(part);
123
+ text = '';
124
+ }
125
+ };
126
+ for (const match of format.matchAll(SPECIFIER)) {
127
+ const specifier = match[1];
128
+ const index = match.index ?? 0;
129
+ text += format.slice(last, index);
130
+ last = index + match[0].length;
131
+ if (specifier === '%') {
132
+ text += '%';
133
+ continue;
134
+ }
135
+ if (consumed >= values.length) {
136
+ text += match[0];
137
+ continue;
138
+ }
139
+ const value = values[consumed++];
140
+ switch (specifier) {
141
+ case 's':
142
+ text += toText(value);
143
+ break;
144
+ case 'd':
145
+ case 'i':
146
+ text += toInteger(value);
147
+ break;
148
+ case 'f':
149
+ text += toFloat(value);
150
+ break;
151
+ case 'c':
152
+ flush();
153
+ style = parseConsoleCss(toText(value));
154
+ break;
155
+ default:
156
+ flush();
157
+ parts.push({ type: 'value', value: capture(value) });
158
+ break;
159
+ }
160
+ }
161
+ text += format.slice(last);
162
+ flush();
163
+ return { parts, rest: values.slice(consumed) };
164
+ };
165
+ /**
166
+ * Turns the arguments of a console call into the parts of an entry.
167
+ *
168
+ * With more than one argument and a string first, the format specifiers are applied. The
169
+ * remaining arguments follow, separated by spaces: strings as plain text and everything else
170
+ * as a captured value.
171
+ */
172
+ export const formatArguments = (args, capture) => {
173
+ let parts = [];
174
+ let rest = [...args];
175
+ if (args.length > 1 && typeof args[0] === 'string') {
176
+ const formatted = applyFormat(args, capture);
177
+ parts = formatted.parts;
178
+ rest = formatted.rest;
179
+ }
180
+ for (const value of rest) {
181
+ if (parts.length > 0) {
182
+ parts.push({ type: 'text', text: ' ' });
183
+ }
184
+ parts.push(typeof value === 'string'
185
+ ? { type: 'text', text: value }
186
+ : { type: 'value', value: capture(value) });
187
+ }
188
+ return parts;
189
+ };
@@ -0,0 +1,27 @@
1
+ import type { LogStore } from '../../core/store.js';
2
+ import { type ConsoleMethod, type RecorderOptions } from './recorder.js';
3
+ export interface HookConsoleOptions extends Partial<RecorderOptions> {
4
+ /** The methods to hook. Defaults to every method in `CONSOLE_METHODS`. */
5
+ methods?: readonly ConsoleMethod[];
6
+ /** Whether the original method still runs, so messages keep reaching the browser console. */
7
+ passthrough?: boolean;
8
+ }
9
+ /** An object with the console methods lognal records. */
10
+ export type LognalConsole = {
11
+ [Method in ConsoleMethod]: (...args: unknown[]) => void;
12
+ };
13
+ /**
14
+ * Replaces the methods of a console so every call is also recorded in a store.
15
+ *
16
+ * Returns a function that restores the original methods. If another script wrapped a method
17
+ * after lognal did, that method is left in place and lognal's wrapper stops recording.
18
+ *
19
+ * A call is recorded before the original method runs, and an error while recording never
20
+ * reaches the page.
21
+ */
22
+ export declare const hookConsole: (target: Console, store: LogStore, options?: HookConsoleOptions) => (() => void);
23
+ /**
24
+ * Creates an object with the console methods that records into a store without touching the
25
+ * global console. Use it to write to a viewer from your own code.
26
+ */
27
+ export declare const createConsole: (store: LogStore, options?: Partial<RecorderOptions>) => LognalConsole;
@@ -0,0 +1,94 @@
1
+ import { CONSOLE_METHODS, ConsoleRecorder } from './recorder.js';
2
+ const FRAME_V8 = /^\s+at\s/;
3
+ const FRAME_OTHER = /@/;
4
+ /** Removes the lines before the first stack frame, such as V8's `Error` header. */
5
+ const framesOnly = (stack) => {
6
+ const lines = stack.split('\n');
7
+ const first = lines.findIndex((line) => FRAME_V8.test(line) || FRAME_OTHER.test(line));
8
+ return first < 0 ? [] : lines.slice(first);
9
+ };
10
+ /**
11
+ * Returns the stack trace of the code that called `wrapper`, without the frames of lognal
12
+ * itself. V8 can leave those frames out on its own; elsewhere the first frame is dropped.
13
+ */
14
+ const captureStack = (wrapper) => {
15
+ const captureStackTrace = Error
16
+ .captureStackTrace;
17
+ if (typeof captureStackTrace === 'function') {
18
+ const holder = {};
19
+ captureStackTrace(holder, wrapper);
20
+ return framesOnly(holder.stack ?? '').join('\n');
21
+ }
22
+ return framesOnly(new Error().stack ?? '')
23
+ .slice(2)
24
+ .join('\n');
25
+ };
26
+ /**
27
+ * Replaces the methods of a console so every call is also recorded in a store.
28
+ *
29
+ * Returns a function that restores the original methods. If another script wrapped a method
30
+ * after lognal did, that method is left in place and lognal's wrapper stops recording.
31
+ *
32
+ * A call is recorded before the original method runs, and an error while recording never
33
+ * reaches the page.
34
+ */
35
+ export const hookConsole = (target, store, options = {}) => {
36
+ const { methods = CONSOLE_METHODS, passthrough = true, ...recorderOptions } = options;
37
+ const recorder = new ConsoleRecorder(store, recorderOptions);
38
+ const console = target;
39
+ const installed = new Map();
40
+ let active = true;
41
+ let recording = false;
42
+ for (const method of methods) {
43
+ if (installed.has(method)) {
44
+ continue;
45
+ }
46
+ const original = console[method];
47
+ const wrapper = function (...args) {
48
+ if (active && !recording) {
49
+ recording = true;
50
+ try {
51
+ recorder.record(method, args, method === 'trace' ? captureStack(wrapper) : undefined);
52
+ }
53
+ catch {
54
+ // Recording must never break the page that logged the message.
55
+ }
56
+ finally {
57
+ recording = false;
58
+ }
59
+ }
60
+ if (passthrough && typeof original === 'function') {
61
+ return original.apply(this, args);
62
+ }
63
+ return undefined;
64
+ };
65
+ console[method] = wrapper;
66
+ installed.set(method, { original, wrapper });
67
+ }
68
+ return () => {
69
+ if (!active) {
70
+ return;
71
+ }
72
+ active = false;
73
+ for (const [method, { original, wrapper }] of installed) {
74
+ if (console[method] === wrapper) {
75
+ console[method] = original;
76
+ }
77
+ }
78
+ };
79
+ };
80
+ /**
81
+ * Creates an object with the console methods that records into a store without touching the
82
+ * global console. Use it to write to a viewer from your own code.
83
+ */
84
+ export const createConsole = (store, options = {}) => {
85
+ const recorder = new ConsoleRecorder(store, options);
86
+ const result = {};
87
+ for (const method of CONSOLE_METHODS) {
88
+ const call = (...args) => {
89
+ recorder.record(method, args, method === 'trace' ? captureStack(call) : undefined);
90
+ };
91
+ result[method] = call;
92
+ }
93
+ return result;
94
+ };
@@ -0,0 +1,41 @@
1
+ import type { LogStore } from '../../core/store.js';
2
+ import { type CaptureOptions } from './snapshot.js';
3
+ /** The console methods lognal records. */
4
+ export type ConsoleMethod = 'log' | 'info' | 'warn' | 'error' | 'debug' | 'trace' | 'dir' | 'dirxml' | 'table' | 'group' | 'groupCollapsed' | 'groupEnd' | 'count' | 'countReset' | 'time' | 'timeLog' | 'timeEnd' | 'assert' | 'clear';
5
+ export declare const CONSOLE_METHODS: readonly ConsoleMethod[];
6
+ export interface RecorderOptions extends CaptureOptions {
7
+ /** Whether `console.clear` removes the entries from the store. */
8
+ clearStore: boolean;
9
+ }
10
+ export declare const DEFAULT_RECORDER_OPTIONS: RecorderOptions;
11
+ /**
12
+ * Turns console calls into store entries.
13
+ *
14
+ * A recorder keeps the state the Console Standard gives a console: the count map, the timer
15
+ * table and the group stack. Everything is captured synchronously, when the method is called.
16
+ */
17
+ export declare class ConsoleRecorder {
18
+ private readonly store;
19
+ private options;
20
+ private readonly counts;
21
+ private readonly timers;
22
+ private groups;
23
+ constructor(store: LogStore, options?: Partial<RecorderOptions>);
24
+ setOptions(options: Partial<RecorderOptions>): void;
25
+ /**
26
+ * Records one call. `stack` is the caller's stack trace, which only `trace` uses.
27
+ */
28
+ record(method: ConsoleMethod, args: readonly unknown[], stack?: string): void;
29
+ private readonly capture;
30
+ private add;
31
+ private trace;
32
+ private table;
33
+ private group;
34
+ private count;
35
+ private countReset;
36
+ private time;
37
+ private timeLog;
38
+ private timeEnd;
39
+ private assert;
40
+ private clear;
41
+ }