@orkestrel/console 0.0.3 → 0.0.4
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/dist/src/browser/index.d.ts +11 -11
- package/dist/src/browser/index.js +39 -36
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +66 -54
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1 -1
- package/dist/src/core/index.d.ts +1 -1
- package/dist/src/core/index.js +66 -54
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +6 -3
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +11 -11
- package/dist/src/server/index.d.ts +11 -11
- package/dist/src/server/index.js +6 -3
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -15
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Color } from '../core/index.
|
|
2
|
-
import { SinkInterface } from '../core/index.
|
|
1
|
+
import { Color } from '../core/index.ts';
|
|
2
|
+
import { SinkInterface } from '../core/index.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Translate an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
|
|
@@ -116,7 +116,7 @@ export declare interface ConsoleOutput {
|
|
|
116
116
|
*
|
|
117
117
|
* @example
|
|
118
118
|
* ```ts
|
|
119
|
-
* import { createLogger } from '
|
|
119
|
+
* import { createLogger } from '@src/core'
|
|
120
120
|
* import { createBrowserSink } from '@src/browser'
|
|
121
121
|
*
|
|
122
122
|
* const logger = createLogger({ name: 'app', sink: createBrowserSink() })
|
|
@@ -176,7 +176,7 @@ export declare function parseParameters(parameters: string): readonly number[];
|
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
178
|
* Matches one SGR sequence (`ESC[ <params> m`) and CAPTURES its `;`-separated numeric parameters —
|
|
179
|
-
* the subset of ANSI {@link import('
|
|
179
|
+
* the subset of ANSI {@link import('@src/core').strip} cares about that carries STYLE (color /
|
|
180
180
|
* attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks
|
|
181
181
|
* every SGR run in a string; built from core's {@link ESC} so no control-character literal appears
|
|
182
182
|
* in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,
|
|
@@ -190,25 +190,25 @@ export declare function parseParameters(parameters: string): readonly number[];
|
|
|
190
190
|
export declare const SGR_PATTERN: RegExp;
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* The
|
|
193
|
+
* The immutable accumulator {@link import('./helpers.js').ansiToConsole} carries across a run while
|
|
194
194
|
* translating SGR codes to CSS — a single `foreground` and `background` declaration (each channel
|
|
195
195
|
* REPLACEABLE by a later color of the same channel) plus an ordered, de-duplicated list of attribute
|
|
196
196
|
* declarations. An SGR reset empties all three; {@link import('./helpers.js').ansiToConsole} folds
|
|
197
197
|
* it into the `;`-joined CSS string a run emits.
|
|
198
198
|
*
|
|
199
199
|
* @remarks
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
200
|
+
* Each SGR sequence produces a new frozen value; earlier run snapshots never drift when a later
|
|
201
|
+
* sequence changes a channel. A channel holds the FULL CSS declaration (`'color:#cd0000'`, not a
|
|
202
|
+
* bare hex), or `''` when unset.
|
|
203
203
|
* - `foreground` — the current `color:<hex>` declaration, or `''` (unset / post-reset).
|
|
204
204
|
* - `background` — the current `background:<hex>` declaration, or `''`.
|
|
205
205
|
* - `attributes` — the active text-effect declarations in insertion order (`'font-weight:bold'`, …),
|
|
206
206
|
* each present at most once.
|
|
207
207
|
*/
|
|
208
208
|
export declare interface StyleAccumulator {
|
|
209
|
-
foreground: string;
|
|
210
|
-
background: string;
|
|
211
|
-
attributes: string[];
|
|
209
|
+
readonly foreground: string;
|
|
210
|
+
readonly background: string;
|
|
211
|
+
readonly attributes: readonly string[];
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
export { }
|
|
@@ -118,63 +118,66 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
|
|
|
118
118
|
*/
|
|
119
119
|
function ansiToConsole(text) {
|
|
120
120
|
const scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags);
|
|
121
|
-
|
|
121
|
+
let active = Object.freeze({
|
|
122
122
|
foreground: "",
|
|
123
123
|
background: "",
|
|
124
|
-
attributes: []
|
|
125
|
-
};
|
|
124
|
+
attributes: Object.freeze([])
|
|
125
|
+
});
|
|
126
126
|
const segments = [];
|
|
127
127
|
const styles = [];
|
|
128
|
-
let styled = false;
|
|
129
128
|
let cursor = 0;
|
|
130
129
|
let pending = "";
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
let match = scanner.exec(text);
|
|
131
|
+
if (match === null) return {
|
|
132
|
+
format: escapePercent(text),
|
|
133
|
+
styles: []
|
|
134
|
+
};
|
|
135
|
+
while (true) {
|
|
136
|
+
const boundary = match === null ? text.length : match.index;
|
|
137
|
+
pending += escapePercent(text.slice(cursor, boundary));
|
|
138
|
+
if (pending !== "") {
|
|
139
|
+
segments.push(`%c${pending}`);
|
|
140
|
+
const declarations = [...active.attributes];
|
|
141
|
+
if (active.foreground !== "") declarations.push(active.foreground);
|
|
142
|
+
if (active.background !== "") declarations.push(active.background);
|
|
143
|
+
styles.push(declarations.join(";"));
|
|
144
|
+
pending = "";
|
|
145
|
+
}
|
|
146
|
+
if (match === null) break;
|
|
147
|
+
for (const code of parseParameters(match[1] ?? "")) {
|
|
133
148
|
if (code === RESET_CODE) {
|
|
134
|
-
active
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
active = Object.freeze({
|
|
150
|
+
foreground: "",
|
|
151
|
+
background: "",
|
|
152
|
+
attributes: Object.freeze([])
|
|
153
|
+
});
|
|
137
154
|
continue;
|
|
138
155
|
}
|
|
139
156
|
const foreground = FOREGROUND_CSS[code];
|
|
140
157
|
if (foreground !== void 0) {
|
|
141
|
-
active
|
|
158
|
+
active = Object.freeze({
|
|
159
|
+
...active,
|
|
160
|
+
foreground
|
|
161
|
+
});
|
|
142
162
|
continue;
|
|
143
163
|
}
|
|
144
164
|
const background = BACKGROUND_CSS[code];
|
|
145
165
|
if (background !== void 0) {
|
|
146
|
-
active
|
|
166
|
+
active = Object.freeze({
|
|
167
|
+
...active,
|
|
168
|
+
background
|
|
169
|
+
});
|
|
147
170
|
continue;
|
|
148
171
|
}
|
|
149
172
|
const attribute = ATTRIBUTE_CSS[code];
|
|
150
|
-
if (attribute !== void 0 && !active.attributes.includes(attribute)) active.
|
|
173
|
+
if (attribute !== void 0 && !active.attributes.includes(attribute)) active = Object.freeze({
|
|
174
|
+
...active,
|
|
175
|
+
attributes: Object.freeze([...active.attributes, attribute])
|
|
176
|
+
});
|
|
151
177
|
}
|
|
152
|
-
};
|
|
153
|
-
const serialize = () => {
|
|
154
|
-
const declarations = [...active.attributes];
|
|
155
|
-
if (active.foreground !== "") declarations.push(active.foreground);
|
|
156
|
-
if (active.background !== "") declarations.push(active.background);
|
|
157
|
-
return declarations.join(";");
|
|
158
|
-
};
|
|
159
|
-
const flush = () => {
|
|
160
|
-
if (pending === "") return;
|
|
161
|
-
segments.push(`%c${pending}`);
|
|
162
|
-
styles.push(serialize());
|
|
163
|
-
pending = "";
|
|
164
|
-
};
|
|
165
|
-
for (let match = scanner.exec(text); match !== null; match = scanner.exec(text)) {
|
|
166
|
-
styled = true;
|
|
167
|
-
pending += escapePercent(text.slice(cursor, match.index));
|
|
168
|
-
flush();
|
|
169
|
-
apply(parseParameters(match[1] ?? ""));
|
|
170
178
|
cursor = match.index + match[0].length;
|
|
179
|
+
match = scanner.exec(text);
|
|
171
180
|
}
|
|
172
|
-
if (!styled) return {
|
|
173
|
-
format: escapePercent(text),
|
|
174
|
-
styles: []
|
|
175
|
-
};
|
|
176
|
-
pending += escapePercent(text.slice(cursor));
|
|
177
|
-
flush();
|
|
178
181
|
return {
|
|
179
182
|
format: segments.join(""),
|
|
180
183
|
styles
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { Color } from '@src/core'\nimport { ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES } from '@src/core'\n\n// The SGR → CSS translation DATA the browser sink maps ANSI runs through (the C-f branch).\n// The core `src/core/console` is the source of truth for the SGR NUMBERS (which code is which\n// color / attribute); this module owns only the BROWSER-side mapping — a named-color → hex\n// palette and each SGR number → its CSS declaration — and DERIVES the number→CSS lookups by\n// walking core's `COLORS` against its code maps + the palette, so the number↔name mapping is\n// never re-hardcoded here. The SGR-scan pattern is built from core's `ESC` so no control-\n// character literal appears in source. UPPER_SNAKE, deeply `Object.freeze`d, every member\n// exported (AGENTS §5).\n\n/**\n * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools\n * console renders the SAME {@link Color} names as. The source of truth for the BROWSER color\n * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to\n * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.\n *\n * @remarks\n * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`\n * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.\n */\nexport const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>> = Object.freeze({\n\tblack: '#000000',\n\tred: '#cd0000',\n\tgreen: '#00cd00',\n\tyellow: '#cdcd00',\n\tblue: '#0000ee',\n\tmagenta: '#cd00cd',\n\tcyan: '#00cdcd',\n\twhite: '#e5e5e5',\n\tbrightBlack: '#7f7f7f',\n\tbrightRed: '#ff0000',\n\tbrightGreen: '#00ff00',\n\tbrightYellow: '#ffff00',\n\tbrightBlue: '#5c5cff',\n\tbrightMagenta: '#ff00ff',\n\tbrightCyan: '#00ffff',\n\tbrightWhite: '#ffffff',\n})\n\n/**\n * Each text-{@link Attribute}'s SGR \"on\" number → its equivalent CSS declaration — the browser\n * counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2 →\n * `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,\n * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`). Keyed by the SGR\n * NUMBER (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly\n * while scanning a run.\n *\n * @remarks\n * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,\n * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as\n * approximate, never silently dropped. Deeply frozen.\n */\nexport const ATTRIBUTE_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[ATTRIBUTE_CODES.bold]: 'font-weight:bold',\n\t[ATTRIBUTE_CODES.dim]: 'opacity:0.6',\n\t[ATTRIBUTE_CODES.italic]: 'font-style:italic',\n\t[ATTRIBUTE_CODES.underline]: 'text-decoration:underline',\n\t[ATTRIBUTE_CODES.inverse]: 'filter:invert(100%)',\n\t[ATTRIBUTE_CODES.strikethrough]: 'text-decoration:line-through',\n})\n\n/**\n * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS, derived by walking core's\n * {@link COLORS} against {@link FOREGROUND_CODES} and {@link COLOR_HEX} (so the number↔name mapping\n * stays in core, never duplicated here). The sink reads this while scanning a run to translate a\n * foreground code to CSS. A pure build-once expression producing a frozen record (the\n * `DEFAULT_CAPTURE_LEVELS = CAPTURE_LEVELS` precedent — derived data, not new data).\n */\nexport const FOREGROUND_CSS: Readonly<Record<number, string>> = Object.freeze(\n\tObject.fromEntries(COLORS.map((color) => [FOREGROUND_CODES[color], `color:${COLOR_HEX[color]}`])),\n)\n\n/**\n * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS, derived by walking\n * core's {@link COLORS} against {@link BACKGROUND_CODES} and {@link COLOR_HEX}. The sink reads this\n * while scanning a run to translate a background code to CSS.\n */\nexport const BACKGROUND_CSS: Readonly<Record<number, string>> = Object.freeze(\n\tObject.fromEntries(\n\t\tCOLORS.map((color) => [BACKGROUND_CODES[color], `background:${COLOR_HEX[color]}`]),\n\t),\n)\n\n/**\n * The browser console directive that switches the active style — one `%c` prefixes every styled run\n * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the\n * parallel CSS array. The single source of truth for the directive token.\n */\nexport const DIRECTIVE = '%c'\n\n/**\n * Matches one SGR sequence (`ESC[ <params> m`) and CAPTURES its `;`-separated numeric parameters —\n * the subset of ANSI {@link import('@src/core').strip} cares about that carries STYLE (color /\n * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks\n * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears\n * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,\n * which the spec treats as a reset).\n *\n * @remarks\n * A global `RegExp` carries a mutable `lastIndex`; a scan builds a FRESH `RegExp` from this one's\n * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the\n * canonical definition, not a shared scanner.\n */\nexport const SGR_PATTERN = new RegExp(`${ESC}\\\\[([0-9;]*)m`, 'g')\n","import type { ConsoleOutput, StyleAccumulator } from './types.js'\nimport { RESET_CODE } from '@src/core'\nimport {\n\tATTRIBUTE_CSS,\n\tBACKGROUND_CSS,\n\tDIRECTIVE,\n\tFOREGROUND_CSS,\n\tSGR_PATTERN,\n} from './constants.js'\n\n// The pure, browser-only translation behind the `%c` console sink (the C-f branch). The core\n// styler / Logger / Reporter emit ANSI-styled STRINGS; a DevTools console can't render ANSI but\n// can style via `console.log('%ctext', 'css')`, so `ansiToConsole` parses the SGR runs in the\n// incoming text and re-emits them as a `%c`-ready format string + parallel CSS array — the\n// translation happens at the OUTPUT boundary, leaving the core unchanged. Pure + total + `%`-safe.\n// `ansiToConsole`'s scan glue (apply / serialize / flush over its own `active` / `segments` /\n// `styles` state) lives as local closures inside it (AGENTS §5); only the standalone, reusable\n// `escapePercent` / `parseParameters` utilities are exported alongside it.\n\n/**\n * Translate an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a\n * `%c`-segmented format string and the parallel array of CSS declarations, so a DevTools console\n * renders the SAME styling a terminal would (the C-f sink calls `console[method](format, ...styles)`).\n *\n * @remarks\n * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —\n * `ESC[…m`); each delimits a run. A run carrying VISIBLE text emits one `%c` directive plus that\n * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at\n * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare\n * `ESC[m`) clears the accumulated style back to none. A later color of the same channel REPLACES\n * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style\n * and are left in the text verbatim.\n * - **`%`-safe.** Every LITERAL `%` in the text is doubled to `%%` so the console never treats it as\n * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`\n * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.\n * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,\n * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).\n * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,\n * lone `ESC`, unterminated sequences all fall through as literal text).\n *\n * @param text - Any string, ANSI-styled or plain\n * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})\n *\n * @example\n * ```ts\n * ansiToConsole('\\x1b[31mred\\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }\n * ansiToConsole('plain') // { format: 'plain', styles: [] }\n * ansiToConsole('50%') // { format: '50%%', styles: [] }\n * ```\n */\nexport function ansiToConsole(text: string): ConsoleOutput {\n\tconst scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags)\n\t// The accumulated active style across a run — a separate foreground / background declaration\n\t// (each channel REPLACEABLE) plus an ordered, de-duplicated list of attribute declarations. An\n\t// SGR reset empties all three. Serialized to a `;`-joined CSS string per emitted run.\n\tconst active: StyleAccumulator = { foreground: '', background: '', attributes: [] }\n\tconst segments: string[] = []\n\tconst styles: string[] = []\n\tlet styled = false\n\tlet cursor = 0\n\tlet pending = ''\n\n\t// Apply one SGR sequence's `codes` to `active` (in place): a reset clears every channel; a\n\t// foreground / background code REPLACES that channel; an attribute is added once (idempotent).\n\t// An unrecognized code (a 256-color / truecolor extension this layer doesn't map) is ignored,\n\t// never raised — keeping the translation total.\n\tconst apply = (codes: readonly number[]): void => {\n\t\tfor (const code of codes) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive.foreground = ''\n\t\t\t\tactive.background = ''\n\t\t\t\tactive.attributes.length = 0\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst foreground = FOREGROUND_CSS[code]\n\t\t\tif (foreground !== undefined) {\n\t\t\t\tactive.foreground = foreground\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst background = BACKGROUND_CSS[code]\n\t\t\tif (background !== undefined) {\n\t\t\t\tactive.background = background\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst attribute = ATTRIBUTE_CSS[code]\n\t\t\tif (attribute !== undefined && !active.attributes.includes(attribute)) {\n\t\t\t\tactive.attributes.push(attribute)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Serialize `active` into one `;`-joined CSS declaration string — attributes (insertion order),\n\t// then foreground, then background, mirroring the renderer's stable code order; an empty style\n\t// (post-reset / nothing accumulated) serializes to `''`.\n\tconst serialize = (): string => {\n\t\tconst declarations = [...active.attributes]\n\t\tif (active.foreground !== '') declarations.push(active.foreground)\n\t\tif (active.background !== '') declarations.push(active.background)\n\t\treturn declarations.join(';')\n\t}\n\n\t// Push `pending` (the run's already-escaped text) as one `%c` segment paired with the run's\n\t// current CSS, then clear `pending`. An EMPTY run is dropped (a style change with no visible text\n\t// emits no `%c`), keeping `format`'s `%c` count exactly equal to `styles.length`.\n\tconst flush = (): void => {\n\t\tif (pending === '') return\n\t\tsegments.push(`${DIRECTIVE}${pending}`)\n\t\tstyles.push(serialize())\n\t\tpending = ''\n\t}\n\n\tfor (let match = scanner.exec(text); match !== null; match = scanner.exec(text)) {\n\t\tstyled = true\n\t\tpending += escapePercent(text.slice(cursor, match.index))\n\t\tflush()\n\t\tapply(parseParameters(match[1] ?? ''))\n\t\tcursor = match.index + match[0].length\n\t}\n\tif (!styled) return { format: escapePercent(text), styles: [] }\n\tpending += escapePercent(text.slice(cursor))\n\tflush()\n\treturn { format: segments.join(''), styles }\n}\n\n/**\n * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (e.g. in `50%` or `%s`) as a format directive. The single escape the\n * {@link ansiToConsole} translation applies to every text segment before assembling the format\n * string (so only the `%c`s it inserts are real directives).\n *\n * @param text - A literal text segment (no inserted directives)\n * @returns `text` with each `%` doubled\n *\n * @example\n * ```ts\n * escapePercent('100% done') // '100%% done'\n * ```\n */\nexport function escapePercent(text: string): string {\n\treturn text.replace(/%/g, '%%')\n}\n\n/**\n * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since\n * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)\n * likewise counts as a `0` reset, matching the spec.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)\n *\n * @example\n * ```ts\n * parseParameters('1;31') // [1, 31]\n * parseParameters('') // [0]\n * ```\n */\nexport function parseParameters(parameters: string): readonly number[] {\n\tif (parameters === '') return [RESET_CODE]\n\treturn parameters.split(';').map((field) => (field === '' ? RESET_CODE : Number(field)))\n}\n","import type { LogLevel, SinkInterface } from '@src/core'\nimport { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the C-f branch) — the platform-bound backend that satisfies core's\n// `SinkInterface` in a browser DevTools console. The core styler / Logger / Reporter emit ANSI-styled\n// STRINGS; a DevTools console can't render ANSI but CAN style via `console.log('%ctext', 'css')`, so\n// this sink translates the incoming ANSI runs into a `%c` call at the OUTPUT boundary (the env-split\n// rule: core owns the contract + universal logic, the browser provides the platform backend). A thin\n// stateless adapter, so a frozen-object factory — like core's `createConsoleSink` — not a class\n// (AGENTS §5). `SinkInterface` / `LogLevel` are IMPORTED from `@src/core`, never redeclared.\n\n/**\n * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in\n * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the\n * core output to the browser console with no change to the core.\n *\n * @returns A browser `%c` {@link SinkInterface}\n *\n * @remarks\n * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and\n * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}\n * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.\n * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an\n * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level\n * reaches the matching DevTools stream.\n * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with\n * a carriage return `\\r` (a spinner / progress redraw) has the leading `\\r` STRIPPED and is written as a\n * fresh, non-overwriting line — the locked browser degrade. Only a LEADING `\\r` is stripped; an interior\n * one is left to the console.\n * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` AT\n * CREATION and writes through those references, so a later `Capture` that PATCHES `console.*` can never\n * feed this sink's output back into itself (the no-capture-loop principle, AGENTS / the core sink's\n * precedent). Create the sink (or the logger) BEFORE installing a capture.\n *\n * @example\n * ```ts\n * import { createLogger } from '@src/core'\n * import { createBrowserSink } from '@src/browser'\n *\n * const logger = createLogger({ name: 'app', sink: createBrowserSink() })\n * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools\n * ```\n */\nexport function createBrowserSink(): SinkInterface {\n\t// Snapshot the three console writers NOW — bound to their `console` receiver — so a later patch of\n\t// `console.*` (by Capture) can never reach this sink's output (no capture loop), exactly as core's\n\t// `createConsoleSink` does.\n\tconst log = console.log.bind(console)\n\tconst warn = console.warn.bind(console)\n\tconst error = console.error.bind(console)\n\treturn {\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\t// Degrade the animation redraw first: a leading `\\r` can't overwrite a line in a browser\n\t\t\t// console, so drop it and write a fresh, non-overwriting line (the locked decision).\n\t\t\tconst line = text.startsWith('\\r') ? text.slice(1) : text\n\t\t\tconst { format, styles } = ansiToConsole(line)\n\t\t\tif (level === 'error') {\n\t\t\t\terror(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\twarn(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,IAAa,YAAiE,OAAO,OAAO;CAC3F,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,MAAM;CACN,SAAS;CACT,MAAM;CACN,OAAO;CACP,aAAa;CACb,WAAW;CACX,aAAa;CACb,cAAc;CACd,YAAY;CACZ,eAAe;CACf,YAAY;CACZ,aAAa;AACd,CAAC;;;;;;;;;;;;;;AAeD,IAAa,gBAAkD,OAAO,OAAO;EAC3E,gBAAgB,OAAO;EACvB,gBAAgB,MAAM;EACtB,gBAAgB,SAAS;EACzB,gBAAgB,YAAY;EAC5B,gBAAgB,UAAU;EAC1B,gBAAgB,gBAAgB;AAClC,CAAC;;;;;;;;AASD,IAAa,iBAAmD,OAAO,OACtE,OAAO,YAAY,OAAO,KAAK,UAAU,CAAC,iBAAiB,QAAQ,SAAS,UAAU,QAAQ,CAAC,CAAC,CACjG;;;;;;AAOA,IAAa,iBAAmD,OAAO,OACtE,OAAO,YACN,OAAO,KAAK,UAAU,CAAC,iBAAiB,QAAQ,cAAc,UAAU,QAAQ,CAAC,CAClF,CACD;;;;;;AAOA,IAAa,YAAY;;;;;;;;;;;;;;AAezB,IAAa,cAAc,IAAI,OAAO,GAAG,IAAI,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvDhE,SAAgB,cAAc,MAA6B;CAC1D,MAAM,UAAU,IAAI,OAAO,YAAY,QAAQ,YAAY,KAAK;CAIhE,MAAM,SAA2B;EAAE,YAAY;EAAI,YAAY;EAAI,YAAY,CAAC;CAAE;CAClF,MAAM,WAAqB,CAAC;CAC5B,MAAM,SAAmB,CAAC;CAC1B,IAAI,SAAS;CACb,IAAI,SAAS;CACb,IAAI,UAAU;CAMd,MAAM,SAAS,UAAmC;EACjD,KAAK,MAAM,QAAQ,OAAO;GACzB,IAAI,SAAS,YAAY;IACxB,OAAO,aAAa;IACpB,OAAO,aAAa;IACpB,OAAO,WAAW,SAAS;IAC3B;GACD;GACA,MAAM,aAAa,eAAe;GAClC,IAAI,eAAe,KAAA,GAAW;IAC7B,OAAO,aAAa;IACpB;GACD;GACA,MAAM,aAAa,eAAe;GAClC,IAAI,eAAe,KAAA,GAAW;IAC7B,OAAO,aAAa;IACpB;GACD;GACA,MAAM,YAAY,cAAc;GAChC,IAAI,cAAc,KAAA,KAAa,CAAC,OAAO,WAAW,SAAS,SAAS,GACnE,OAAO,WAAW,KAAK,SAAS;EAElC;CACD;CAKA,MAAM,kBAA0B;EAC/B,MAAM,eAAe,CAAC,GAAG,OAAO,UAAU;EAC1C,IAAI,OAAO,eAAe,IAAI,aAAa,KAAK,OAAO,UAAU;EACjE,IAAI,OAAO,eAAe,IAAI,aAAa,KAAK,OAAO,UAAU;EACjE,OAAO,aAAa,KAAK,GAAG;CAC7B;CAKA,MAAM,cAAoB;EACzB,IAAI,YAAY,IAAI;EACpB,SAAS,KAAK,KAAe,SAAS;EACtC,OAAO,KAAK,UAAU,CAAC;EACvB,UAAU;CACX;CAEA,KAAK,IAAI,QAAQ,QAAQ,KAAK,IAAI,GAAG,UAAU,MAAM,QAAQ,QAAQ,KAAK,IAAI,GAAG;EAChF,SAAS;EACT,WAAW,cAAc,KAAK,MAAM,QAAQ,MAAM,KAAK,CAAC;EACxD,MAAM;EACN,MAAM,gBAAgB,MAAM,MAAM,EAAE,CAAC;EACrC,SAAS,MAAM,QAAQ,MAAM,EAAE,CAAC;CACjC;CACA,IAAI,CAAC,QAAQ,OAAO;EAAE,QAAQ,cAAc,IAAI;EAAG,QAAQ,CAAC;CAAE;CAC9D,WAAW,cAAc,KAAK,MAAM,MAAM,CAAC;CAC3C,MAAM;CACN,OAAO;EAAE,QAAQ,SAAS,KAAK,EAAE;EAAG;CAAO;AAC5C;;;;;;;;;;;;;;;AAgBA,SAAgB,cAAc,MAAsB;CACnD,OAAO,KAAK,QAAQ,MAAM,IAAI;AAC/B;;;;;;;;;;;;;;;;AAiBA,SAAgB,gBAAgB,YAAuC;CACtE,IAAI,eAAe,IAAI,OAAO,CAAC,UAAU;CACzC,OAAO,WAAW,MAAM,GAAG,CAAC,CAAC,KAAK,UAAW,UAAU,KAAK,aAAa,OAAO,KAAK,CAAE;AACxF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACnHA,SAAgB,oBAAmC;CAIlD,MAAM,MAAM,QAAQ,IAAI,KAAK,OAAO;CACpC,MAAM,OAAO,QAAQ,KAAK,KAAK,OAAO;CACtC,MAAM,QAAQ,QAAQ,MAAM,KAAK,OAAO;CACxC,OAAO,EACN,MAAM,MAAc,OAAwB;EAI3C,MAAM,EAAE,QAAQ,WAAW,cADd,KAAK,WAAW,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,IACR;EAC7C,IAAI,UAAU,SAAS;GACtB,MAAM,QAAQ,GAAG,MAAM;GACvB;EACD;EACA,IAAI,UAAU,QAAQ;GACrB,KAAK,QAAQ,GAAG,MAAM;GACtB;EACD;EACA,IAAI,QAAQ,GAAG,MAAM;CACtB,EACD;AACD"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { Color } from '@src/core'\nimport { ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES } from '@src/core'\n\n// The SGR → CSS translation DATA the browser sink maps ANSI runs through (the C-f branch).\n// The core `src/core/console` is the source of truth for the SGR NUMBERS (which code is which\n// color / attribute); this module owns only the BROWSER-side mapping — a named-color → hex\n// palette and each SGR number → its CSS declaration — and DERIVES the number→CSS lookups by\n// walking core's `COLORS` against its code maps + the palette, so the number↔name mapping is\n// never re-hardcoded here. The SGR-scan pattern is built from core's `ESC` so no control-\n// character literal appears in source. UPPER_SNAKE, deeply `Object.freeze`d, every member\n// exported (AGENTS §5).\n\n/**\n * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools\n * console renders the SAME {@link Color} names as. The source of truth for the BROWSER color\n * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to\n * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.\n *\n * @remarks\n * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`\n * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.\n */\nexport const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>> = Object.freeze({\n\tblack: '#000000',\n\tred: '#cd0000',\n\tgreen: '#00cd00',\n\tyellow: '#cdcd00',\n\tblue: '#0000ee',\n\tmagenta: '#cd00cd',\n\tcyan: '#00cdcd',\n\twhite: '#e5e5e5',\n\tbrightBlack: '#7f7f7f',\n\tbrightRed: '#ff0000',\n\tbrightGreen: '#00ff00',\n\tbrightYellow: '#ffff00',\n\tbrightBlue: '#5c5cff',\n\tbrightMagenta: '#ff00ff',\n\tbrightCyan: '#00ffff',\n\tbrightWhite: '#ffffff',\n})\n\n/**\n * Each text-{@link Attribute}'s SGR \"on\" number → its equivalent CSS declaration — the browser\n * counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2 →\n * `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,\n * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`). Keyed by the SGR\n * NUMBER (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly\n * while scanning a run.\n *\n * @remarks\n * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,\n * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as\n * approximate, never silently dropped. Deeply frozen.\n */\nexport const ATTRIBUTE_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[ATTRIBUTE_CODES.bold]: 'font-weight:bold',\n\t[ATTRIBUTE_CODES.dim]: 'opacity:0.6',\n\t[ATTRIBUTE_CODES.italic]: 'font-style:italic',\n\t[ATTRIBUTE_CODES.underline]: 'text-decoration:underline',\n\t[ATTRIBUTE_CODES.inverse]: 'filter:invert(100%)',\n\t[ATTRIBUTE_CODES.strikethrough]: 'text-decoration:line-through',\n})\n\n/**\n * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS, derived by walking core's\n * {@link COLORS} against {@link FOREGROUND_CODES} and {@link COLOR_HEX} (so the number↔name mapping\n * stays in core, never duplicated here). The sink reads this while scanning a run to translate a\n * foreground code to CSS. A pure build-once expression producing a frozen record (the\n * `DEFAULT_CAPTURE_LEVELS = CAPTURE_LEVELS` precedent — derived data, not new data).\n */\nexport const FOREGROUND_CSS: Readonly<Record<number, string>> = Object.freeze(\n\tObject.fromEntries(COLORS.map((color) => [FOREGROUND_CODES[color], `color:${COLOR_HEX[color]}`])),\n)\n\n/**\n * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS, derived by walking\n * core's {@link COLORS} against {@link BACKGROUND_CODES} and {@link COLOR_HEX}. The sink reads this\n * while scanning a run to translate a background code to CSS.\n */\nexport const BACKGROUND_CSS: Readonly<Record<number, string>> = Object.freeze(\n\tObject.fromEntries(\n\t\tCOLORS.map((color) => [BACKGROUND_CODES[color], `background:${COLOR_HEX[color]}`]),\n\t),\n)\n\n/**\n * The browser console directive that switches the active style — one `%c` prefixes every styled run\n * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the\n * parallel CSS array. The single source of truth for the directive token.\n */\nexport const DIRECTIVE = '%c'\n\n/**\n * Matches one SGR sequence (`ESC[ <params> m`) and CAPTURES its `;`-separated numeric parameters —\n * the subset of ANSI {@link import('@src/core').strip} cares about that carries STYLE (color /\n * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks\n * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears\n * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,\n * which the spec treats as a reset).\n *\n * @remarks\n * A global `RegExp` carries a mutable `lastIndex`; a scan builds a FRESH `RegExp` from this one's\n * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the\n * canonical definition, not a shared scanner.\n */\nexport const SGR_PATTERN = new RegExp(`${ESC}\\\\[([0-9;]*)m`, 'g')\n","import type { ConsoleOutput, StyleAccumulator } from './types.js'\nimport { RESET_CODE } from '@src/core'\nimport {\n\tATTRIBUTE_CSS,\n\tBACKGROUND_CSS,\n\tDIRECTIVE,\n\tFOREGROUND_CSS,\n\tSGR_PATTERN,\n} from './constants.js'\n\n// The pure, browser-only translation behind the `%c` console sink (the C-f branch). The core\n// styler / Logger / Reporter emit ANSI-styled STRINGS; a DevTools console can't render ANSI but\n// can style via `console.log('%ctext', 'css')`, so `ansiToConsole` parses the SGR runs in the\n// incoming text and re-emits them as a `%c`-ready format string + parallel CSS array — the\n// translation happens at the OUTPUT boundary, leaving the core unchanged. Pure + total + `%`-safe.\n// `ansiToConsole` carries immutable style snapshots while its local arrays assemble the final\n// `%c` output; only the standalone, reusable `escapePercent` / `parseParameters` utilities are\n// exported alongside it.\n\n/**\n * Translate an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a\n * `%c`-segmented format string and the parallel array of CSS declarations, so a DevTools console\n * renders the SAME styling a terminal would (the C-f sink calls `console[method](format, ...styles)`).\n *\n * @remarks\n * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —\n * `ESC[…m`); each delimits a run. A run carrying VISIBLE text emits one `%c` directive plus that\n * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at\n * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare\n * `ESC[m`) clears the accumulated style back to none. A later color of the same channel REPLACES\n * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style\n * and are left in the text verbatim.\n * - **`%`-safe.** Every LITERAL `%` in the text is doubled to `%%` so the console never treats it as\n * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`\n * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.\n * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,\n * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).\n * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,\n * lone `ESC`, unterminated sequences all fall through as literal text).\n *\n * @param text - Any string, ANSI-styled or plain\n * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})\n *\n * @example\n * ```ts\n * ansiToConsole('\\x1b[31mred\\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }\n * ansiToConsole('plain') // { format: 'plain', styles: [] }\n * ansiToConsole('50%') // { format: '50%%', styles: [] }\n * ```\n */\nexport function ansiToConsole(text: string): ConsoleOutput {\n\tconst scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags)\n\t// The accumulated active style across a run — a separate foreground / background declaration\n\t// (each channel REPLACEABLE) plus an ordered, de-duplicated list of attribute declarations. An\n\t// SGR reset empties all three. Serialized to a `;`-joined CSS string per emitted run.\n\tlet active: StyleAccumulator = Object.freeze({\n\t\tforeground: '',\n\t\tbackground: '',\n\t\tattributes: Object.freeze([]),\n\t})\n\tconst segments: string[] = []\n\tconst styles: string[] = []\n\tlet cursor = 0\n\tlet pending = ''\n\tlet match: RegExpExecArray | null = scanner.exec(text)\n\tif (match === null) return { format: escapePercent(text), styles: [] }\n\n\t// A null match is the final text boundary, so every visible run passes through one flush path.\n\twhile (true) {\n\t\tconst boundary = match === null ? text.length : match.index\n\t\tpending += escapePercent(text.slice(cursor, boundary))\n\t\tif (pending !== '') {\n\t\t\tsegments.push(`${DIRECTIVE}${pending}`)\n\t\t\tconst declarations = [...active.attributes]\n\t\t\tif (active.foreground !== '') declarations.push(active.foreground)\n\t\t\tif (active.background !== '') declarations.push(active.background)\n\t\t\tstyles.push(declarations.join(';'))\n\t\t\tpending = ''\n\t\t}\n\t\tif (match === null) break\n\n\t\t// Apply one SGR sequence by replacing the readonly accumulator. A reset clears every channel;\n\t\t// colors replace their channel; attributes accumulate once; unknown extensions are ignored.\n\t\tfor (const code of parseParameters(match[1] ?? '')) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\tforeground: '',\n\t\t\t\t\tbackground: '',\n\t\t\t\t\tattributes: Object.freeze([]),\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst foreground = FOREGROUND_CSS[code]\n\t\t\tif (foreground !== undefined) {\n\t\t\t\tactive = Object.freeze({ ...active, foreground })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst background = BACKGROUND_CSS[code]\n\t\t\tif (background !== undefined) {\n\t\t\t\tactive = Object.freeze({ ...active, background })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst attribute = ATTRIBUTE_CSS[code]\n\t\t\tif (attribute !== undefined && !active.attributes.includes(attribute)) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\t...active,\n\t\t\t\t\tattributes: Object.freeze([...active.attributes, attribute]),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tcursor = match.index + match[0].length\n\t\tmatch = scanner.exec(text)\n\t}\n\treturn { format: segments.join(''), styles }\n}\n\n/**\n * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (e.g. in `50%` or `%s`) as a format directive. The single escape the\n * {@link ansiToConsole} translation applies to every text segment before assembling the format\n * string (so only the `%c`s it inserts are real directives).\n *\n * @param text - A literal text segment (no inserted directives)\n * @returns `text` with each `%` doubled\n *\n * @example\n * ```ts\n * escapePercent('100% done') // '100%% done'\n * ```\n */\nexport function escapePercent(text: string): string {\n\treturn text.replace(/%/g, '%%')\n}\n\n/**\n * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since\n * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)\n * likewise counts as a `0` reset, matching the spec.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)\n *\n * @example\n * ```ts\n * parseParameters('1;31') // [1, 31]\n * parseParameters('') // [0]\n * ```\n */\nexport function parseParameters(parameters: string): readonly number[] {\n\tif (parameters === '') return [RESET_CODE]\n\treturn parameters.split(';').map((field) => (field === '' ? RESET_CODE : Number(field)))\n}\n","import type { LogLevel, SinkInterface } from '@src/core'\nimport { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the C-f branch) — the platform-bound backend that satisfies core's\n// `SinkInterface` in a browser DevTools console. The core styler / Logger / Reporter emit ANSI-styled\n// STRINGS; a DevTools console can't render ANSI but CAN style via `console.log('%ctext', 'css')`, so\n// this sink translates the incoming ANSI runs into a `%c` call at the OUTPUT boundary (the env-split\n// rule: core owns the contract + universal logic, the browser provides the platform backend). A thin\n// stateless adapter, so a frozen-object factory — like core's `createConsoleSink` — not a class\n// (AGENTS §5). `SinkInterface` / `LogLevel` are IMPORTED from `@src/core`, never redeclared.\n\n/**\n * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in\n * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the\n * core output to the browser console with no change to the core.\n *\n * @returns A browser `%c` {@link SinkInterface}\n *\n * @remarks\n * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and\n * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}\n * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.\n * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an\n * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level\n * reaches the matching DevTools stream.\n * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with\n * a carriage return `\\r` (a spinner / progress redraw) has the leading `\\r` STRIPPED and is written as a\n * fresh, non-overwriting line — the locked browser degrade. Only a LEADING `\\r` is stripped; an interior\n * one is left to the console.\n * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` AT\n * CREATION and writes through those references, so a later `Capture` that PATCHES `console.*` can never\n * feed this sink's output back into itself (the no-capture-loop principle, AGENTS / the core sink's\n * precedent). Create the sink (or the logger) BEFORE installing a capture.\n *\n * @example\n * ```ts\n * import { createLogger } from '@src/core'\n * import { createBrowserSink } from '@src/browser'\n *\n * const logger = createLogger({ name: 'app', sink: createBrowserSink() })\n * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools\n * ```\n */\nexport function createBrowserSink(): SinkInterface {\n\t// Snapshot the three console writers NOW — bound to their `console` receiver — so a later patch of\n\t// `console.*` (by Capture) can never reach this sink's output (no capture loop), exactly as core's\n\t// `createConsoleSink` does.\n\tconst log = console.log.bind(console)\n\tconst warn = console.warn.bind(console)\n\tconst error = console.error.bind(console)\n\treturn {\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\t// Degrade the animation redraw first: a leading `\\r` can't overwrite a line in a browser\n\t\t\t// console, so drop it and write a fresh, non-overwriting line (the locked decision).\n\t\t\tconst line = text.startsWith('\\r') ? text.slice(1) : text\n\t\t\tconst { format, styles } = ansiToConsole(line)\n\t\t\tif (level === 'error') {\n\t\t\t\terror(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\twarn(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -431,7 +431,7 @@ var ConsoleError = class extends Error {
|
|
|
431
431
|
super(message);
|
|
432
432
|
this.name = "ConsoleError";
|
|
433
433
|
this.code = code;
|
|
434
|
-
this.context = context;
|
|
434
|
+
if (context !== void 0) this.context = context;
|
|
435
435
|
}
|
|
436
436
|
};
|
|
437
437
|
/**
|
|
@@ -755,20 +755,21 @@ function renderBox(options) {
|
|
|
755
755
|
const inner = lines.reduce((max, line) => Math.max(max, width(line)), Math.max(0, titleRoom, budget));
|
|
756
756
|
const gutter = " ".repeat(padding);
|
|
757
757
|
const bar = paint(styler, chars.vertical);
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
758
|
+
const span = inner + padding * 2;
|
|
759
|
+
let top;
|
|
760
|
+
if (options.title === void 0) top = paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`);
|
|
761
|
+
else {
|
|
761
762
|
const caption = ` ${options.title} `;
|
|
762
763
|
const room = span - width(caption);
|
|
763
764
|
const lead = paint(styler, repeatTo(chars.horizontal, 1));
|
|
764
765
|
const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1));
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
const top = boxTop();
|
|
766
|
+
top = `${paint(styler, chars.topLeft)}${lead}${paint(styler, caption)}${rest}${paint(styler, chars.topRight)}`;
|
|
767
|
+
}
|
|
768
768
|
const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`);
|
|
769
|
+
const body = lines.map((line) => `${bar}${gutter}${align(line, inner)}${gutter}${bar}`);
|
|
769
770
|
return [
|
|
770
771
|
top,
|
|
771
|
-
...
|
|
772
|
+
...body,
|
|
772
773
|
bottom
|
|
773
774
|
].join("\n");
|
|
774
775
|
}
|
|
@@ -799,22 +800,20 @@ function renderTable(options) {
|
|
|
799
800
|
const columns = options.columns;
|
|
800
801
|
const widths = columns.map((column, index) => options.rows.reduce((max, row) => Math.max(max, width(cellAt(row, index))), width(column.label)));
|
|
801
802
|
const aligns = columns.map((column) => column.align ?? "left");
|
|
802
|
-
const
|
|
803
|
+
const renderedRows = [columns.map((column) => paint(styler, column.label)), ...options.rows.map((row) => columns.map((_column, index) => cellAt(row, index)))].map((cells) => {
|
|
803
804
|
const bar = paint(styler, chars.vertical);
|
|
804
805
|
return `${bar}${cells.map((cell, index) => ` ${align(cell, widths[index] ?? width(cell), aligns[index] ?? "left")} `).join(bar)}${bar}`;
|
|
805
|
-
};
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
};
|
|
810
|
-
const header = tableRow(columns.map((column) => paint(styler, column.label)));
|
|
811
|
-
const body = options.rows.map((row) => tableRow(columns.map((_column, index) => cellAt(row, index))));
|
|
806
|
+
});
|
|
807
|
+
const segments = widths.map((columnWidth) => repeatTo(chars.horizontal, columnWidth + 2));
|
|
808
|
+
const top = paint(styler, `${chars.topLeft}${segments.join(chars.teeDown)}${chars.topRight}`);
|
|
809
|
+
const rule = paint(styler, `${chars.teeRight}${segments.join(chars.cross)}${chars.teeLeft}`);
|
|
810
|
+
const bottom = paint(styler, `${chars.bottomLeft}${segments.join(chars.teeUp)}${chars.bottomRight}`);
|
|
812
811
|
return [
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
...
|
|
817
|
-
|
|
812
|
+
top,
|
|
813
|
+
...renderedRows.slice(0, 1),
|
|
814
|
+
rule,
|
|
815
|
+
...renderedRows.slice(1),
|
|
816
|
+
bottom
|
|
818
817
|
].join("\n");
|
|
819
818
|
}
|
|
820
819
|
/**
|
|
@@ -1076,7 +1075,7 @@ var Styler = class Styler {
|
|
|
1076
1075
|
* type assertion is used (AGENTS §1 / §14 — narrow, never assert).
|
|
1077
1076
|
*/
|
|
1078
1077
|
get surface() {
|
|
1079
|
-
const render =
|
|
1078
|
+
const render = this.#render.bind(this);
|
|
1080
1079
|
const descriptors = {
|
|
1081
1080
|
style: {
|
|
1082
1081
|
value: this.#style,
|
|
@@ -1088,17 +1087,26 @@ var Styler = class Styler {
|
|
|
1088
1087
|
}
|
|
1089
1088
|
};
|
|
1090
1089
|
for (const color of COLORS) descriptors[color] = {
|
|
1091
|
-
get:
|
|
1090
|
+
get: this.#foregroundSurface.bind(this, color),
|
|
1092
1091
|
enumerable: true
|
|
1093
1092
|
};
|
|
1094
1093
|
for (const attribute of ATTRIBUTES) descriptors[attribute] = {
|
|
1095
|
-
get:
|
|
1094
|
+
get: this.#attributeSurface.bind(this, attribute),
|
|
1096
1095
|
enumerable: true
|
|
1097
1096
|
};
|
|
1098
1097
|
const surface = Object.defineProperties(render, descriptors);
|
|
1099
1098
|
if (this.#isSurface(surface)) return surface;
|
|
1100
1099
|
throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
|
|
1101
1100
|
}
|
|
1101
|
+
#render(text) {
|
|
1102
|
+
return this.#enabled ? this.#renderer.render(this.#style, text) : text;
|
|
1103
|
+
}
|
|
1104
|
+
#foregroundSurface(color) {
|
|
1105
|
+
return this.#foreground(color).surface;
|
|
1106
|
+
}
|
|
1107
|
+
#attributeSurface(attribute) {
|
|
1108
|
+
return this.#attribute(attribute).surface;
|
|
1109
|
+
}
|
|
1102
1110
|
#isSurface(value) {
|
|
1103
1111
|
return typeof value === "function" && "style" in value && "enabled" in value && "red" in value && "bold" in value;
|
|
1104
1112
|
}
|
|
@@ -1161,8 +1169,8 @@ var Capture = class {
|
|
|
1161
1169
|
#active = false;
|
|
1162
1170
|
constructor(options) {
|
|
1163
1171
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1164
|
-
on: options
|
|
1165
|
-
error: options
|
|
1172
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
1173
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
1166
1174
|
});
|
|
1167
1175
|
this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
|
|
1168
1176
|
this.#mirror = options?.mirror ?? false;
|
|
@@ -1184,7 +1192,7 @@ var Capture = class {
|
|
|
1184
1192
|
const original = target[level];
|
|
1185
1193
|
this.#originals.set(level, original);
|
|
1186
1194
|
const mirror = original.bind(console);
|
|
1187
|
-
target[level] =
|
|
1195
|
+
target[level] = this.#captureCall.bind(this, level, mirror);
|
|
1188
1196
|
}
|
|
1189
1197
|
this.#emitter.emit("start");
|
|
1190
1198
|
}
|
|
@@ -1208,6 +1216,9 @@ var Capture = class {
|
|
|
1208
1216
|
this.stop();
|
|
1209
1217
|
this.#emitter.destroy();
|
|
1210
1218
|
}
|
|
1219
|
+
#captureCall(level, mirror, ...args) {
|
|
1220
|
+
this.#intercept(level, args, mirror);
|
|
1221
|
+
}
|
|
1211
1222
|
#intercept(level, args, mirror) {
|
|
1212
1223
|
const message = this.#capture(level, args);
|
|
1213
1224
|
this.#retain(message);
|
|
@@ -1286,11 +1297,11 @@ var LoggerManager = class {
|
|
|
1286
1297
|
}
|
|
1287
1298
|
register(name, options) {
|
|
1288
1299
|
const logger = new Logger({
|
|
1289
|
-
level: this.#level,
|
|
1290
|
-
sink: this.#sink,
|
|
1291
|
-
styler: this.#styler,
|
|
1292
|
-
limit: this.#limit,
|
|
1293
|
-
silent: this.#silent,
|
|
1300
|
+
...this.#level !== void 0 ? { level: this.#level } : {},
|
|
1301
|
+
...this.#sink !== void 0 ? { sink: this.#sink } : {},
|
|
1302
|
+
...this.#styler !== void 0 ? { styler: this.#styler } : {},
|
|
1303
|
+
...this.#limit !== void 0 ? { limit: this.#limit } : {},
|
|
1304
|
+
...this.#silent !== void 0 ? { silent: this.#silent } : {},
|
|
1294
1305
|
...options,
|
|
1295
1306
|
name
|
|
1296
1307
|
});
|
|
@@ -1373,8 +1384,8 @@ var Progress = class {
|
|
|
1373
1384
|
#completed = false;
|
|
1374
1385
|
constructor(options) {
|
|
1375
1386
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1376
|
-
on: options.on,
|
|
1377
|
-
error: options.error
|
|
1387
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
1388
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
1378
1389
|
});
|
|
1379
1390
|
this.#total = options.total;
|
|
1380
1391
|
this.#width = options.width ?? 30;
|
|
@@ -1572,8 +1583,8 @@ var Spinner = class {
|
|
|
1572
1583
|
#index = 0;
|
|
1573
1584
|
constructor(options) {
|
|
1574
1585
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1575
|
-
on: options
|
|
1576
|
-
error: options
|
|
1586
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
1587
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
1577
1588
|
});
|
|
1578
1589
|
const frames = options?.frames ?? SPINNER_FRAMES;
|
|
1579
1590
|
this.#frames = frames.length === 0 ? SPINNER_FRAMES : frames;
|
|
@@ -1910,21 +1921,25 @@ function createCapture(options) {
|
|
|
1910
1921
|
function withCapture(fn, options) {
|
|
1911
1922
|
const capture = new Capture(options);
|
|
1912
1923
|
capture.start();
|
|
1913
|
-
const settle = (value) => {
|
|
1914
|
-
const messages = capture.messages();
|
|
1915
|
-
capture.destroy();
|
|
1916
|
-
return {
|
|
1917
|
-
value,
|
|
1918
|
-
messages
|
|
1919
|
-
};
|
|
1920
|
-
};
|
|
1921
1924
|
try {
|
|
1922
1925
|
const result = fn();
|
|
1923
|
-
if (result instanceof Promise) return result.then((value) =>
|
|
1926
|
+
if (result instanceof Promise) return result.then((value) => {
|
|
1927
|
+
const messages = capture.messages();
|
|
1928
|
+
capture.destroy();
|
|
1929
|
+
return {
|
|
1930
|
+
value,
|
|
1931
|
+
messages
|
|
1932
|
+
};
|
|
1933
|
+
}, (error) => {
|
|
1924
1934
|
capture.destroy();
|
|
1925
1935
|
throw error;
|
|
1926
1936
|
});
|
|
1927
|
-
|
|
1937
|
+
const messages = capture.messages();
|
|
1938
|
+
capture.destroy();
|
|
1939
|
+
return {
|
|
1940
|
+
value: result,
|
|
1941
|
+
messages
|
|
1942
|
+
};
|
|
1928
1943
|
} catch (error) {
|
|
1929
1944
|
capture.destroy();
|
|
1930
1945
|
throw error;
|
|
@@ -2029,19 +2044,19 @@ function createProgress(options) {
|
|
|
2029
2044
|
var Logger = class {
|
|
2030
2045
|
#emitter;
|
|
2031
2046
|
#level;
|
|
2032
|
-
#name;
|
|
2033
2047
|
#sink;
|
|
2034
2048
|
#styler;
|
|
2035
2049
|
#limit;
|
|
2036
2050
|
#silent;
|
|
2037
2051
|
#entries = [];
|
|
2052
|
+
name;
|
|
2038
2053
|
constructor(options) {
|
|
2039
2054
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
2040
|
-
on: options
|
|
2041
|
-
error: options
|
|
2055
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
2056
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
2042
2057
|
});
|
|
2043
2058
|
this.#level = options?.level ?? "info";
|
|
2044
|
-
this
|
|
2059
|
+
if (options?.name !== void 0) this.name = options.name;
|
|
2045
2060
|
this.#sink = options?.sink ?? createConsoleSink();
|
|
2046
2061
|
this.#styler = options?.styler ?? createStyler();
|
|
2047
2062
|
this.#limit = options?.limit ?? 1e3;
|
|
@@ -2053,9 +2068,6 @@ var Logger = class {
|
|
|
2053
2068
|
get level() {
|
|
2054
2069
|
return this.#level;
|
|
2055
2070
|
}
|
|
2056
|
-
get name() {
|
|
2057
|
-
return this.#name;
|
|
2058
|
-
}
|
|
2059
2071
|
debug(message, data) {
|
|
2060
2072
|
this.#log("debug", message, data);
|
|
2061
2073
|
}
|
|
@@ -2091,7 +2103,7 @@ var Logger = class {
|
|
|
2091
2103
|
level,
|
|
2092
2104
|
message,
|
|
2093
2105
|
time: Date.now(),
|
|
2094
|
-
...this
|
|
2106
|
+
...this.name === void 0 ? {} : { name: this.name },
|
|
2095
2107
|
...data === void 0 ? {} : { data: Object.freeze({ ...data }) }
|
|
2096
2108
|
});
|
|
2097
2109
|
}
|