@orkestrel/console 0.0.4 → 0.0.6
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 +17 -9
- package/dist/src/browser/index.js +52 -10
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +325 -326
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +3 -48
- package/dist/src/core/index.d.ts +3 -48
- package/dist/src/core/index.js +326 -326
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.d.cts +2 -2
- package/dist/src/server/index.d.ts +2 -2
- package/package.json +8 -6
package/dist/src/core/index.js
CHANGED
|
@@ -452,6 +452,127 @@ function isConsoleError(value) {
|
|
|
452
452
|
return value instanceof ConsoleError;
|
|
453
453
|
}
|
|
454
454
|
//#endregion
|
|
455
|
+
//#region src/core/Capture.ts
|
|
456
|
+
/**
|
|
457
|
+
* An observable console interceptor (AGENTS §13) — it takes control of the global `console.*` on
|
|
458
|
+
* the READ side. While `active`, every configured `console.x` call is captured as a frozen
|
|
459
|
+
* {@link CapturedMessage}, buffered (total + by level, bounded), emitted on `capture`, and — per
|
|
460
|
+
* options — mirrored to the real console and/or forwarded to a {@link SinkInterface}.
|
|
461
|
+
*
|
|
462
|
+
* @remarks
|
|
463
|
+
* - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the CURRENT
|
|
464
|
+
* `console[level]` for each configured {@link CaptureLevel}, then installs the wrappers. The
|
|
465
|
+
* mirror writes through that snapshot — so our OWN console sink output (the Logger / Reporter,
|
|
466
|
+
* which snapshot the real `console` at creation) is never recaptured: `Capture` catches
|
|
467
|
+
* THIRD-PARTY `console.*`, not our writes. Create your loggers BEFORE installing a capture.
|
|
468
|
+
* - **Idempotent + PROCESS-GLOBAL + NON-REENTRANT.** `start()` while already `active` is a no-op
|
|
469
|
+
* (never double-patches); `stop()` while inactive is a no-op. It patches the ONE global
|
|
470
|
+
* `console`, so at most ONE capture may be active at a time — running two concurrently
|
|
471
|
+
* interleaves their buffers and clobbers each other's restore.
|
|
472
|
+
* - **Bounded buffers.** `messages()` / `messages(level)` — the total buffer and each by-level
|
|
473
|
+
* bucket are each capped at `limit`
|
|
474
|
+
* (oldest dropped first), never unbounded — the same retention precedent as {@link Logger}.
|
|
475
|
+
* - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);
|
|
476
|
+
* `destroy()` stops (restoring `console`) then destroys the emitter.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```ts
|
|
480
|
+
* const capture = new Capture({ levels: ['warn', 'error'], mirror: true })
|
|
481
|
+
* capture.start()
|
|
482
|
+
* console.warn('third-party noise') // captured AND mirrored to the real console
|
|
483
|
+
* capture.messages('warn') // [{ level: 'warn', text: 'third-party noise', time: … }]
|
|
484
|
+
* capture.stop() // console.warn restored
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
var Capture = class {
|
|
488
|
+
#emitter;
|
|
489
|
+
#levels;
|
|
490
|
+
#mirror;
|
|
491
|
+
#sink;
|
|
492
|
+
#limit;
|
|
493
|
+
#messages = [];
|
|
494
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
495
|
+
#originals = /* @__PURE__ */ new Map();
|
|
496
|
+
#active = false;
|
|
497
|
+
constructor(options) {
|
|
498
|
+
this.#emitter = new Emitter({
|
|
499
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
500
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
501
|
+
});
|
|
502
|
+
this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
|
|
503
|
+
this.#mirror = options?.mirror ?? false;
|
|
504
|
+
this.#sink = options?.sink;
|
|
505
|
+
this.#limit = options?.limit ?? 1e3;
|
|
506
|
+
for (const level of this.#levels) this.#buckets.set(level, []);
|
|
507
|
+
}
|
|
508
|
+
get emitter() {
|
|
509
|
+
return this.#emitter;
|
|
510
|
+
}
|
|
511
|
+
get active() {
|
|
512
|
+
return this.#active;
|
|
513
|
+
}
|
|
514
|
+
start() {
|
|
515
|
+
if (this.#active) return;
|
|
516
|
+
this.#active = true;
|
|
517
|
+
const target = console;
|
|
518
|
+
for (const level of this.#levels) {
|
|
519
|
+
const original = target[level];
|
|
520
|
+
this.#originals.set(level, original);
|
|
521
|
+
const mirror = original.bind(console);
|
|
522
|
+
target[level] = this.#captureCall.bind(this, level, mirror);
|
|
523
|
+
}
|
|
524
|
+
this.#emitter.emit("start");
|
|
525
|
+
}
|
|
526
|
+
stop() {
|
|
527
|
+
if (!this.#active) return;
|
|
528
|
+
this.#active = false;
|
|
529
|
+
const target = console;
|
|
530
|
+
for (const [level, original] of this.#originals) target[level] = original;
|
|
531
|
+
this.#originals.clear();
|
|
532
|
+
this.#emitter.emit("stop");
|
|
533
|
+
}
|
|
534
|
+
messages(level) {
|
|
535
|
+
if (level === void 0) return [...this.#messages];
|
|
536
|
+
return [...this.#buckets.get(level) ?? []];
|
|
537
|
+
}
|
|
538
|
+
clear() {
|
|
539
|
+
this.#messages.length = 0;
|
|
540
|
+
for (const bucket of this.#buckets.values()) bucket.length = 0;
|
|
541
|
+
}
|
|
542
|
+
destroy() {
|
|
543
|
+
this.stop();
|
|
544
|
+
this.#emitter.destroy();
|
|
545
|
+
}
|
|
546
|
+
#captureCall(level, mirror, ...args) {
|
|
547
|
+
this.#intercept(level, args, mirror);
|
|
548
|
+
}
|
|
549
|
+
#intercept(level, args, mirror) {
|
|
550
|
+
const message = this.#capture(level, args);
|
|
551
|
+
this.#retain(message);
|
|
552
|
+
this.#emitter.emit("capture", message);
|
|
553
|
+
if (this.#mirror) mirror(...args);
|
|
554
|
+
if (this.#sink !== void 0) try {
|
|
555
|
+
this.#sink.write(message.text, CAPTURE_LEVEL_MAP[level]);
|
|
556
|
+
} catch {}
|
|
557
|
+
}
|
|
558
|
+
#capture(level, args) {
|
|
559
|
+
return Object.freeze({
|
|
560
|
+
level,
|
|
561
|
+
text: formatArgs(args),
|
|
562
|
+
time: Date.now()
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
#retain(message) {
|
|
566
|
+
this.#push(this.#messages, message);
|
|
567
|
+
const bucket = this.#buckets.get(message.level);
|
|
568
|
+
if (bucket !== void 0) this.#push(bucket, message);
|
|
569
|
+
}
|
|
570
|
+
#push(buffer, message) {
|
|
571
|
+
buffer.push(message);
|
|
572
|
+
if (buffer.length > this.#limit) buffer.shift();
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
//#endregion
|
|
455
576
|
//#region src/core/helpers.ts
|
|
456
577
|
/**
|
|
457
578
|
* Remove every ANSI escape sequence from `text`, returning the plain visible string.
|
|
@@ -977,271 +1098,116 @@ function renderBar(options) {
|
|
|
977
1098
|
const filledCells = Math.round(fraction * track);
|
|
978
1099
|
return `${`${paint(options.styler, repeatTo(fill, filledCells))}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
|
|
979
1100
|
}
|
|
980
|
-
//#endregion
|
|
981
|
-
//#region src/core/ANSIRenderer.ts
|
|
982
|
-
/**
|
|
983
|
-
* The cross-environment default {@link RendererInterface} — renders style DATA as ANSI
|
|
984
|
-
* SGR escape codes, exactly as `Scheduler` is the `setTimeout` default for its seam. It
|
|
985
|
-
* is the single styling output the whole console / terminal system uses in a terminal;
|
|
986
|
-
* the browser `%c` / CSS renderer (the C-f branch) implements the SAME contract over
|
|
987
|
-
* the SAME {@link Style}, so retargeting changes the renderer, never the style model.
|
|
988
|
-
*
|
|
989
|
-
* @remarks
|
|
990
|
-
* - **Style is DATA in, SGR string out.** It reads the style's `foreground` /
|
|
991
|
-
* `background` / `attributes` and emits one `ESC[…m` sequence whose parameters are the
|
|
992
|
-
* mapped SGR numbers (foreground 30–37 / 90–97, background 40–47 / 100–107, attributes
|
|
993
|
-
* 1 / 2 / 3 / 4 / 7 / 9), followed by `text`, terminated by the reset `ESC[0m`.
|
|
994
|
-
* - **Multiple attributes compose** — their codes join with `;` in a single sequence
|
|
995
|
-
* (`ESC[1;4;31m` for bold + underline + red), so one open and one reset wrap the run.
|
|
996
|
-
* - **`default` and unset colors emit no code** — a `default` (or absent) `foreground` /
|
|
997
|
-
* `background` leaves the terminal's own ink.
|
|
998
|
-
* - **The empty style and the empty string pass through** — when there is nothing to
|
|
999
|
-
* apply (no colors, no attributes) or `text` is `''`, `text` is returned VERBATIM with
|
|
1000
|
-
* no escape codes, so an unstyled render never injects a stray reset.
|
|
1001
|
-
* - **Stateless and event-free** — no fields, no events; safe to share one instance.
|
|
1002
|
-
*/
|
|
1003
|
-
var ANSIRenderer = class {
|
|
1004
|
-
/**
|
|
1005
|
-
* Wrap `text` in the SGR codes for `style`. Returns `text` unchanged when the style
|
|
1006
|
-
* is empty or `text` is `''`.
|
|
1007
|
-
*/
|
|
1008
|
-
render(style, text) {
|
|
1009
|
-
if (text === "") return text;
|
|
1010
|
-
const codes = this.#codes(style);
|
|
1011
|
-
if (codes.length === 0) return text;
|
|
1012
|
-
return `${CSI}${codes.join(";")}m${text}${RESET}`;
|
|
1013
|
-
}
|
|
1014
|
-
#codes(style) {
|
|
1015
|
-
const codes = [];
|
|
1016
|
-
for (const attribute of style.attributes) codes.push(ATTRIBUTE_CODES[attribute]);
|
|
1017
|
-
if (style.foreground !== void 0 && style.foreground !== "default") codes.push(FOREGROUND_CODES[style.foreground]);
|
|
1018
|
-
if (style.background !== void 0 && style.background !== "default") codes.push(BACKGROUND_CODES[style.background]);
|
|
1019
|
-
return codes;
|
|
1020
|
-
}
|
|
1021
|
-
};
|
|
1022
|
-
//#endregion
|
|
1023
|
-
//#region src/core/Styler.ts
|
|
1024
1101
|
/**
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
* {@link
|
|
1028
|
-
* color / attribute accessor is immutable copy-on-write: it returns a NEW styler's
|
|
1029
|
-
* surface with the token added, so `styler.red.bold('hi')` composes without mutating,
|
|
1030
|
-
* and a base styler is freely reusable.
|
|
1102
|
+
* Run `fn` with the global `console.*` captured for its duration, returning the function's `value`
|
|
1103
|
+
* plus the {@link import('./types.js').CapturedMessage}s it logged — the scoped, self-restoring
|
|
1104
|
+
* ergonomic form of {@link createCapture}.
|
|
1031
1105
|
*
|
|
1032
|
-
* @
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
* engine behind it.
|
|
1039
|
-
* - **Immutable.** `#foreground` and `#attribute` return a fresh `Styler` (the style is
|
|
1040
|
-
* rebuilt, never mutated). A later color of the same channel WINS (last write); a
|
|
1041
|
-
* repeated attribute is idempotent (de-duplicated, order preserved).
|
|
1042
|
-
* - **`enabled` switch.** When `false`, the render function returns text VERBATIM — no
|
|
1043
|
-
* renderer call, no escape codes (for a non-TTY / `NO_COLOR` / piped output).
|
|
1044
|
-
* - **Event-free** — a pure styling primitive (AGENTS §13), like `Scheduler`.
|
|
1045
|
-
*/
|
|
1046
|
-
var Styler = class Styler {
|
|
1047
|
-
#renderer;
|
|
1048
|
-
#enabled;
|
|
1049
|
-
#style;
|
|
1050
|
-
constructor(renderer, enabled, style) {
|
|
1051
|
-
this.#renderer = renderer;
|
|
1052
|
-
this.#enabled = enabled;
|
|
1053
|
-
this.#style = style;
|
|
1054
|
-
}
|
|
1055
|
-
/** The accumulated style DATA — the empty style on a base styler. */
|
|
1056
|
-
get style() {
|
|
1057
|
-
return this.#style;
|
|
1058
|
-
}
|
|
1059
|
-
/** Whether styling is applied; when `false`, the surface returns text unchanged. */
|
|
1060
|
-
get enabled() {
|
|
1061
|
-
return this.#enabled;
|
|
1062
|
-
}
|
|
1063
|
-
/**
|
|
1064
|
-
* The fluent {@link StylerInterface} value — a render function (`text => string`) with
|
|
1065
|
-
* `style`, `enabled`, and every {@link Color} / {@link Attribute} as a LAZY accessor
|
|
1066
|
-
* (each computes the next styler's surface only when read). This is what consumers
|
|
1067
|
-
* hold and call.
|
|
1068
|
-
*
|
|
1069
|
-
* @remarks
|
|
1070
|
-
* The accessors are defined as getters (not eagerly-merged values), so accessing one
|
|
1071
|
-
* builds exactly one child styler — the tree is never fully materialized and the
|
|
1072
|
-
* construction terminates. The assembled function is then narrowed to
|
|
1073
|
-
* {@link StylerInterface} through {@link #isSurface} (a real structural check), so no
|
|
1074
|
-
* type assertion is used (AGENTS §1 / §14 — narrow, never assert).
|
|
1075
|
-
*/
|
|
1076
|
-
get surface() {
|
|
1077
|
-
const render = this.#render.bind(this);
|
|
1078
|
-
const descriptors = {
|
|
1079
|
-
style: {
|
|
1080
|
-
value: this.#style,
|
|
1081
|
-
enumerable: true
|
|
1082
|
-
},
|
|
1083
|
-
enabled: {
|
|
1084
|
-
value: this.#enabled,
|
|
1085
|
-
enumerable: true
|
|
1086
|
-
}
|
|
1087
|
-
};
|
|
1088
|
-
for (const color of COLORS) descriptors[color] = {
|
|
1089
|
-
get: this.#foregroundSurface.bind(this, color),
|
|
1090
|
-
enumerable: true
|
|
1091
|
-
};
|
|
1092
|
-
for (const attribute of ATTRIBUTES) descriptors[attribute] = {
|
|
1093
|
-
get: this.#attributeSurface.bind(this, attribute),
|
|
1094
|
-
enumerable: true
|
|
1095
|
-
};
|
|
1096
|
-
const surface = Object.defineProperties(render, descriptors);
|
|
1097
|
-
if (this.#isSurface(surface)) return surface;
|
|
1098
|
-
throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
|
|
1099
|
-
}
|
|
1100
|
-
#render(text) {
|
|
1101
|
-
return this.#enabled ? this.#renderer.render(this.#style, text) : text;
|
|
1102
|
-
}
|
|
1103
|
-
#foregroundSurface(color) {
|
|
1104
|
-
return this.#foreground(color).surface;
|
|
1105
|
-
}
|
|
1106
|
-
#attributeSurface(attribute) {
|
|
1107
|
-
return this.#attribute(attribute).surface;
|
|
1108
|
-
}
|
|
1109
|
-
#isSurface(value) {
|
|
1110
|
-
return typeof value === "function" && "style" in value && "enabled" in value && "red" in value && "bold" in value;
|
|
1111
|
-
}
|
|
1112
|
-
#foreground(color) {
|
|
1113
|
-
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1114
|
-
...this.#style,
|
|
1115
|
-
foreground: color
|
|
1116
|
-
}));
|
|
1117
|
-
}
|
|
1118
|
-
#attribute(attribute) {
|
|
1119
|
-
if (this.#style.attributes.includes(attribute)) return this;
|
|
1120
|
-
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1121
|
-
...this.#style,
|
|
1122
|
-
attributes: Object.freeze([...this.#style.attributes, attribute])
|
|
1123
|
-
}));
|
|
1124
|
-
}
|
|
1125
|
-
};
|
|
1126
|
-
//#endregion
|
|
1127
|
-
//#region src/core/Capture.ts
|
|
1128
|
-
/**
|
|
1129
|
-
* An observable console interceptor (AGENTS §13) — it takes control of the global `console.*` on
|
|
1130
|
-
* the READ side. While `active`, every configured `console.x` call is captured as a frozen
|
|
1131
|
-
* {@link CapturedMessage}, buffered (total + by level, bounded), emitted on `capture`, and — per
|
|
1132
|
-
* options — mirrored to the real console and/or forwarded to a {@link SinkInterface}.
|
|
1106
|
+
* @param fn - The function to run under capture; may be sync (returns `T`) or async (returns
|
|
1107
|
+
* `Promise<T>`)
|
|
1108
|
+
* @param options - See {@link CaptureOptions} (`levels` / `mirror` / `sink` / `limit` / `on` /
|
|
1109
|
+
* `error`); the capture is started for the duration of `fn` regardless
|
|
1110
|
+
* @returns For a sync `fn`, a {@link CaptureResult}`<T>` (`{ value, messages }`); for an async
|
|
1111
|
+
* `fn`, a `Promise<CaptureResult<T>>` (awaited, then console restored)
|
|
1133
1112
|
*
|
|
1134
1113
|
* @remarks
|
|
1135
|
-
* - **
|
|
1136
|
-
* `
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
* `
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
* bucket are each capped at `limit`
|
|
1146
|
-
* (oldest dropped first), never unbounded — the same retention precedent as {@link Logger}.
|
|
1147
|
-
* - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);
|
|
1148
|
-
* `destroy()` stops (restoring `console`) then destroys the emitter.
|
|
1114
|
+
* - **Always restores.** `start()` runs before `fn`; `stop()` runs in a `finally`, so `console` is
|
|
1115
|
+
* restored even if `fn` throws / rejects (the throw / rejection still propagates). The capture
|
|
1116
|
+
* is local — created, used, and destroyed within the call.
|
|
1117
|
+
* - **Sync vs async.** A `fn` returning a `Promise` is detected and AWAITED before `stop()`, so
|
|
1118
|
+
* captures during the async work are included; a plain `fn` stops synchronously. The return type
|
|
1119
|
+
* follows `fn`'s (overloaded).
|
|
1120
|
+
* - **PROCESS-GLOBAL caveat.** Like {@link createCapture}, this patches the one global `console`.
|
|
1121
|
+
* Concurrent `withCapture` calls (or a `withCapture` around other capturing code) INTERLEAVE —
|
|
1122
|
+
* each captures every `console.*` call in flight, and the inner `stop()` restores whatever the
|
|
1123
|
+
* outer had installed. Use it for sequential, scoped capture, not overlapping captures.
|
|
1149
1124
|
*
|
|
1150
1125
|
* @example
|
|
1151
1126
|
* ```ts
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
return this.#active;
|
|
1185
|
-
}
|
|
1186
|
-
start() {
|
|
1187
|
-
if (this.#active) return;
|
|
1188
|
-
this.#active = true;
|
|
1189
|
-
const target = console;
|
|
1190
|
-
for (const level of this.#levels) {
|
|
1191
|
-
const original = target[level];
|
|
1192
|
-
this.#originals.set(level, original);
|
|
1193
|
-
const mirror = original.bind(console);
|
|
1194
|
-
target[level] = this.#captureCall.bind(this, level, mirror);
|
|
1195
|
-
}
|
|
1196
|
-
this.#emitter.emit("start");
|
|
1197
|
-
}
|
|
1198
|
-
stop() {
|
|
1199
|
-
if (!this.#active) return;
|
|
1200
|
-
this.#active = false;
|
|
1201
|
-
const target = console;
|
|
1202
|
-
for (const [level, original] of this.#originals) target[level] = original;
|
|
1203
|
-
this.#originals.clear();
|
|
1204
|
-
this.#emitter.emit("stop");
|
|
1205
|
-
}
|
|
1206
|
-
messages(level) {
|
|
1207
|
-
if (level === void 0) return [...this.#messages];
|
|
1208
|
-
return [...this.#buckets.get(level) ?? []];
|
|
1209
|
-
}
|
|
1210
|
-
clear() {
|
|
1211
|
-
this.#messages.length = 0;
|
|
1212
|
-
for (const bucket of this.#buckets.values()) bucket.length = 0;
|
|
1213
|
-
}
|
|
1214
|
-
destroy() {
|
|
1215
|
-
this.stop();
|
|
1216
|
-
this.#emitter.destroy();
|
|
1217
|
-
}
|
|
1218
|
-
#captureCall(level, mirror, ...args) {
|
|
1219
|
-
this.#intercept(level, args, mirror);
|
|
1220
|
-
}
|
|
1221
|
-
#intercept(level, args, mirror) {
|
|
1222
|
-
const message = this.#capture(level, args);
|
|
1223
|
-
this.#retain(message);
|
|
1224
|
-
this.#emitter.emit("capture", message);
|
|
1225
|
-
if (this.#mirror) mirror(...args);
|
|
1226
|
-
if (this.#sink !== void 0) try {
|
|
1227
|
-
this.#sink.write(message.text, CAPTURE_LEVEL_MAP[level]);
|
|
1228
|
-
} catch {}
|
|
1229
|
-
}
|
|
1230
|
-
#capture(level, args) {
|
|
1231
|
-
return Object.freeze({
|
|
1232
|
-
level,
|
|
1233
|
-
text: formatArgs(args),
|
|
1234
|
-
time: Date.now()
|
|
1127
|
+
* import { withCapture } from '@src/core'
|
|
1128
|
+
*
|
|
1129
|
+
* const { value, messages } = withCapture(() => {
|
|
1130
|
+
* console.log('working')
|
|
1131
|
+
* return 42
|
|
1132
|
+
* })
|
|
1133
|
+
* value // 42
|
|
1134
|
+
* messages.map((m) => m.text) // ['working']
|
|
1135
|
+
*
|
|
1136
|
+
* // Async — awaited before console is restored.
|
|
1137
|
+
* const out = await withCapture(async () => {
|
|
1138
|
+
* console.warn('async noise')
|
|
1139
|
+
* return 'done'
|
|
1140
|
+
* })
|
|
1141
|
+
* out.value // 'done'
|
|
1142
|
+
* ```
|
|
1143
|
+
*/
|
|
1144
|
+
function withCapture(fn, options) {
|
|
1145
|
+
const capture = new Capture(options);
|
|
1146
|
+
capture.start();
|
|
1147
|
+
try {
|
|
1148
|
+
const result = fn();
|
|
1149
|
+
if (result instanceof Promise) return result.then((value) => {
|
|
1150
|
+
const messages = capture.messages();
|
|
1151
|
+
capture.destroy();
|
|
1152
|
+
return {
|
|
1153
|
+
value,
|
|
1154
|
+
messages
|
|
1155
|
+
};
|
|
1156
|
+
}, (error) => {
|
|
1157
|
+
capture.destroy();
|
|
1158
|
+
throw error;
|
|
1235
1159
|
});
|
|
1160
|
+
const messages = capture.messages();
|
|
1161
|
+
capture.destroy();
|
|
1162
|
+
return {
|
|
1163
|
+
value: result,
|
|
1164
|
+
messages
|
|
1165
|
+
};
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
capture.destroy();
|
|
1168
|
+
throw error;
|
|
1236
1169
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1170
|
+
}
|
|
1171
|
+
//#endregion
|
|
1172
|
+
//#region src/core/ANSIRenderer.ts
|
|
1173
|
+
/**
|
|
1174
|
+
* The cross-environment default {@link RendererInterface} — renders style DATA as ANSI
|
|
1175
|
+
* SGR escape codes, exactly as `Scheduler` is the `setTimeout` default for its seam. It
|
|
1176
|
+
* is the single styling output the whole console / terminal system uses in a terminal;
|
|
1177
|
+
* the browser `%c` / CSS renderer (the C-f branch) implements the SAME contract over
|
|
1178
|
+
* the SAME {@link Style}, so retargeting changes the renderer, never the style model.
|
|
1179
|
+
*
|
|
1180
|
+
* @remarks
|
|
1181
|
+
* - **Style is DATA in, SGR string out.** It reads the style's `foreground` /
|
|
1182
|
+
* `background` / `attributes` and emits one `ESC[…m` sequence whose parameters are the
|
|
1183
|
+
* mapped SGR numbers (foreground 30–37 / 90–97, background 40–47 / 100–107, attributes
|
|
1184
|
+
* 1 / 2 / 3 / 4 / 7 / 9), followed by `text`, terminated by the reset `ESC[0m`.
|
|
1185
|
+
* - **Multiple attributes compose** — their codes join with `;` in a single sequence
|
|
1186
|
+
* (`ESC[1;4;31m` for bold + underline + red), so one open and one reset wrap the run.
|
|
1187
|
+
* - **`default` and unset colors emit no code** — a `default` (or absent) `foreground` /
|
|
1188
|
+
* `background` leaves the terminal's own ink.
|
|
1189
|
+
* - **The empty style and the empty string pass through** — when there is nothing to
|
|
1190
|
+
* apply (no colors, no attributes) or `text` is `''`, `text` is returned VERBATIM with
|
|
1191
|
+
* no escape codes, so an unstyled render never injects a stray reset.
|
|
1192
|
+
* - **Stateless and event-free** — no fields, no events; safe to share one instance.
|
|
1193
|
+
*/
|
|
1194
|
+
var ANSIRenderer = class {
|
|
1195
|
+
/**
|
|
1196
|
+
* Wrap `text` in the SGR codes for `style`. Returns `text` unchanged when the style
|
|
1197
|
+
* is empty or `text` is `''`.
|
|
1198
|
+
*/
|
|
1199
|
+
render(style, text) {
|
|
1200
|
+
if (text === "") return text;
|
|
1201
|
+
const codes = this.#codes(style);
|
|
1202
|
+
if (codes.length === 0) return text;
|
|
1203
|
+
return `${CSI}${codes.join(";")}m${text}${RESET}`;
|
|
1241
1204
|
}
|
|
1242
|
-
#
|
|
1243
|
-
|
|
1244
|
-
|
|
1205
|
+
#codes(style) {
|
|
1206
|
+
const codes = [];
|
|
1207
|
+
for (const attribute of style.attributes) codes.push(ATTRIBUTE_CODES[attribute]);
|
|
1208
|
+
if (style.foreground !== void 0 && style.foreground !== "default") codes.push(FOREGROUND_CODES[style.foreground]);
|
|
1209
|
+
if (style.background !== void 0 && style.background !== "default") codes.push(BACKGROUND_CODES[style.background]);
|
|
1210
|
+
return codes;
|
|
1245
1211
|
}
|
|
1246
1212
|
};
|
|
1247
1213
|
//#endregion
|
|
@@ -1650,6 +1616,110 @@ var Spinner = class {
|
|
|
1650
1616
|
}
|
|
1651
1617
|
};
|
|
1652
1618
|
//#endregion
|
|
1619
|
+
//#region src/core/Styler.ts
|
|
1620
|
+
/**
|
|
1621
|
+
* The fluent, composable styler — the consumer-facing API over the style engine. It
|
|
1622
|
+
* builds a {@link Style} (style as DATA) and renders it through an injected
|
|
1623
|
+
* {@link RendererInterface} (the ANSI default, or a browser `%c` renderer at C-f). Each
|
|
1624
|
+
* color / attribute accessor is immutable copy-on-write: it returns a NEW styler's
|
|
1625
|
+
* surface with the token added, so `styler.red.bold('hi')` composes without mutating,
|
|
1626
|
+
* and a base styler is freely reusable.
|
|
1627
|
+
*
|
|
1628
|
+
* @remarks
|
|
1629
|
+
* - **Callable surface.** A `Styler` is not itself callable; its {@link surface} getter
|
|
1630
|
+
* returns the {@link StylerInterface} — a render FUNCTION carrying the chainable
|
|
1631
|
+
* accessors. The accessors are installed as LAZY getters (`Object.defineProperties`),
|
|
1632
|
+
* so a chain materializes only the stylers it actually walks — never the full tree —
|
|
1633
|
+
* and the recursion terminates. The factory returns that surface; this class is the
|
|
1634
|
+
* engine behind it.
|
|
1635
|
+
* - **Immutable.** `#foreground` and `#attribute` return a fresh `Styler` (the style is
|
|
1636
|
+
* rebuilt, never mutated). A later color of the same channel WINS (last write); a
|
|
1637
|
+
* repeated attribute is idempotent (de-duplicated, order preserved).
|
|
1638
|
+
* - **`enabled` switch.** When `false`, the render function returns text VERBATIM — no
|
|
1639
|
+
* renderer call, no escape codes (for a non-TTY / `NO_COLOR` / piped output).
|
|
1640
|
+
* - **Event-free** — a pure styling primitive (AGENTS §13), like `Scheduler`.
|
|
1641
|
+
*/
|
|
1642
|
+
var Styler = class Styler {
|
|
1643
|
+
#renderer;
|
|
1644
|
+
#enabled;
|
|
1645
|
+
#style;
|
|
1646
|
+
constructor(renderer, enabled, style) {
|
|
1647
|
+
this.#renderer = renderer;
|
|
1648
|
+
this.#enabled = enabled;
|
|
1649
|
+
this.#style = style;
|
|
1650
|
+
}
|
|
1651
|
+
/** The accumulated style DATA — the empty style on a base styler. */
|
|
1652
|
+
get style() {
|
|
1653
|
+
return this.#style;
|
|
1654
|
+
}
|
|
1655
|
+
/** Whether styling is applied; when `false`, the surface returns text unchanged. */
|
|
1656
|
+
get enabled() {
|
|
1657
|
+
return this.#enabled;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* The fluent {@link StylerInterface} value — a render function (`text => string`) with
|
|
1661
|
+
* `style`, `enabled`, and every {@link Color} / {@link Attribute} as a LAZY accessor
|
|
1662
|
+
* (each computes the next styler's surface only when read). This is what consumers
|
|
1663
|
+
* hold and call.
|
|
1664
|
+
*
|
|
1665
|
+
* @remarks
|
|
1666
|
+
* The accessors are defined as getters (not eagerly-merged values), so accessing one
|
|
1667
|
+
* builds exactly one child styler — the tree is never fully materialized and the
|
|
1668
|
+
* construction terminates. The assembled function is then narrowed to
|
|
1669
|
+
* {@link StylerInterface} through {@link #isSurface} (a real structural check), so no
|
|
1670
|
+
* type assertion is used (AGENTS §1 / §14 — narrow, never assert).
|
|
1671
|
+
*/
|
|
1672
|
+
get surface() {
|
|
1673
|
+
const render = this.#render.bind(this);
|
|
1674
|
+
const descriptors = {
|
|
1675
|
+
style: {
|
|
1676
|
+
value: this.#style,
|
|
1677
|
+
enumerable: true
|
|
1678
|
+
},
|
|
1679
|
+
enabled: {
|
|
1680
|
+
value: this.#enabled,
|
|
1681
|
+
enumerable: true
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
for (const color of COLORS) descriptors[color] = {
|
|
1685
|
+
get: this.#foregroundSurface.bind(this, color),
|
|
1686
|
+
enumerable: true
|
|
1687
|
+
};
|
|
1688
|
+
for (const attribute of ATTRIBUTES) descriptors[attribute] = {
|
|
1689
|
+
get: this.#attributeSurface.bind(this, attribute),
|
|
1690
|
+
enumerable: true
|
|
1691
|
+
};
|
|
1692
|
+
const surface = Object.defineProperties(render, descriptors);
|
|
1693
|
+
if (this.#isSurface(surface)) return surface;
|
|
1694
|
+
throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
|
|
1695
|
+
}
|
|
1696
|
+
#render(text) {
|
|
1697
|
+
return this.#enabled ? this.#renderer.render(this.#style, text) : text;
|
|
1698
|
+
}
|
|
1699
|
+
#foregroundSurface(color) {
|
|
1700
|
+
return this.#foreground(color).surface;
|
|
1701
|
+
}
|
|
1702
|
+
#attributeSurface(attribute) {
|
|
1703
|
+
return this.#attribute(attribute).surface;
|
|
1704
|
+
}
|
|
1705
|
+
#isSurface(value) {
|
|
1706
|
+
return typeof value === "function" && "style" in value && "enabled" in value && "red" in value && "bold" in value;
|
|
1707
|
+
}
|
|
1708
|
+
#foreground(color) {
|
|
1709
|
+
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1710
|
+
...this.#style,
|
|
1711
|
+
foreground: color
|
|
1712
|
+
}));
|
|
1713
|
+
}
|
|
1714
|
+
#attribute(attribute) {
|
|
1715
|
+
if (this.#style.attributes.includes(attribute)) return this;
|
|
1716
|
+
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1717
|
+
...this.#style,
|
|
1718
|
+
attributes: Object.freeze([...this.#style.attributes, attribute])
|
|
1719
|
+
}));
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
//#endregion
|
|
1653
1723
|
//#region src/core/factories.ts
|
|
1654
1724
|
/**
|
|
1655
1725
|
* Create the cross-environment default {@link RendererInterface} — the ANSI / SGR
|
|
@@ -1875,76 +1945,6 @@ function createCapture(options) {
|
|
|
1875
1945
|
return new Capture(options);
|
|
1876
1946
|
}
|
|
1877
1947
|
/**
|
|
1878
|
-
* Run `fn` with the global `console.*` captured for its duration, returning the function's `value`
|
|
1879
|
-
* plus the {@link import('./types.js').CapturedMessage}s it logged — the scoped, self-restoring
|
|
1880
|
-
* ergonomic form of {@link createCapture}.
|
|
1881
|
-
*
|
|
1882
|
-
* @param fn - The function to run under capture; may be sync (returns `T`) or async (returns
|
|
1883
|
-
* `Promise<T>`)
|
|
1884
|
-
* @param options - See {@link CaptureOptions} (`levels` / `mirror` / `sink` / `limit` / `on` /
|
|
1885
|
-
* `error`); the capture is started for the duration of `fn` regardless
|
|
1886
|
-
* @returns For a sync `fn`, a {@link CaptureResult}`<T>` (`{ value, messages }`); for an async
|
|
1887
|
-
* `fn`, a `Promise<CaptureResult<T>>` (awaited, then console restored)
|
|
1888
|
-
*
|
|
1889
|
-
* @remarks
|
|
1890
|
-
* - **Always restores.** `start()` runs before `fn`; `stop()` runs in a `finally`, so `console` is
|
|
1891
|
-
* restored even if `fn` throws / rejects (the throw / rejection still propagates). The capture
|
|
1892
|
-
* is local — created, used, and destroyed within the call.
|
|
1893
|
-
* - **Sync vs async.** A `fn` returning a `Promise` is detected and AWAITED before `stop()`, so
|
|
1894
|
-
* captures during the async work are included; a plain `fn` stops synchronously. The return type
|
|
1895
|
-
* follows `fn`'s (overloaded).
|
|
1896
|
-
* - **PROCESS-GLOBAL caveat.** Like {@link createCapture}, this patches the one global `console`.
|
|
1897
|
-
* Concurrent `withCapture` calls (or a `withCapture` around other capturing code) INTERLEAVE —
|
|
1898
|
-
* each captures every `console.*` call in flight, and the inner `stop()` restores whatever the
|
|
1899
|
-
* outer had installed. Use it for sequential, scoped capture, not overlapping captures.
|
|
1900
|
-
*
|
|
1901
|
-
* @example
|
|
1902
|
-
* ```ts
|
|
1903
|
-
* import { withCapture } from '@src/core'
|
|
1904
|
-
*
|
|
1905
|
-
* const { value, messages } = withCapture(() => {
|
|
1906
|
-
* console.log('working')
|
|
1907
|
-
* return 42
|
|
1908
|
-
* })
|
|
1909
|
-
* value // 42
|
|
1910
|
-
* messages.map((m) => m.text) // ['working']
|
|
1911
|
-
*
|
|
1912
|
-
* // Async — awaited before console is restored.
|
|
1913
|
-
* const out = await withCapture(async () => {
|
|
1914
|
-
* console.warn('async noise')
|
|
1915
|
-
* return 'done'
|
|
1916
|
-
* })
|
|
1917
|
-
* out.value // 'done'
|
|
1918
|
-
* ```
|
|
1919
|
-
*/
|
|
1920
|
-
function withCapture(fn, options) {
|
|
1921
|
-
const capture = new Capture(options);
|
|
1922
|
-
capture.start();
|
|
1923
|
-
try {
|
|
1924
|
-
const result = fn();
|
|
1925
|
-
if (result instanceof Promise) return result.then((value) => {
|
|
1926
|
-
const messages = capture.messages();
|
|
1927
|
-
capture.destroy();
|
|
1928
|
-
return {
|
|
1929
|
-
value,
|
|
1930
|
-
messages
|
|
1931
|
-
};
|
|
1932
|
-
}, (error) => {
|
|
1933
|
-
capture.destroy();
|
|
1934
|
-
throw error;
|
|
1935
|
-
});
|
|
1936
|
-
const messages = capture.messages();
|
|
1937
|
-
capture.destroy();
|
|
1938
|
-
return {
|
|
1939
|
-
value: result,
|
|
1940
|
-
messages
|
|
1941
|
-
};
|
|
1942
|
-
} catch (error) {
|
|
1943
|
-
capture.destroy();
|
|
1944
|
-
throw error;
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
/**
|
|
1948
1948
|
* Create a self-driving, observable {@link SpinnerInterface} — a live activity spinner. `start()`
|
|
1949
1949
|
* arms a periodic timer that advances a glyph cycle, writing each `\r` + frame line to its sink and
|
|
1950
1950
|
* emitting it on `frame`; `success` / `failure` commit a final `✔` / `✖` line. The leading `\r` is the
|
|
@@ -2112,6 +2112,6 @@ var Logger = class {
|
|
|
2112
2112
|
}
|
|
2113
2113
|
};
|
|
2114
2114
|
//#endregion
|
|
2115
|
-
export { ANSIRenderer, ANSI_PATTERN, ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, BAR_EMPTY, BAR_FILL, BEL, BORDER_CHARS, CAPTURE_LEVELS, CAPTURE_LEVEL_MAP, COLORS, CONTROL_PATTERN, CSI, Capture, ConsoleError, DEFAULT_ALIGN, DEFAULT_BAR_WIDTH, DEFAULT_BORDER, DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_LIMIT, DEFAULT_PADDING, DEFAULT_SPINNER_INTERVAL, DEFAULT_WIDTH, EMPTY_STYLE, ESC, FOREGROUND_CODES, LEVELS, LEVEL_COLORS, LEVEL_SEVERITY, Logger, LoggerManager, Progress, RESET, RESET_CODE, Reporter, SECOND_MS, SEPARATOR_FILL, SEPARATOR_TITLE_GAP, SPINNER_FRAMES, STATUS_COLORS, STATUS_ICONS, STATUS_LEVELS, Spinner,
|
|
2115
|
+
export { ANSIRenderer, ANSI_PATTERN, ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, BAR_EMPTY, BAR_FILL, BEL, BORDER_CHARS, CAPTURE_LEVELS, CAPTURE_LEVEL_MAP, COLORS, CONTROL_PATTERN, CSI, Capture, ConsoleError, DEFAULT_ALIGN, DEFAULT_BAR_WIDTH, DEFAULT_BORDER, DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_LIMIT, DEFAULT_PADDING, DEFAULT_SPINNER_INTERVAL, DEFAULT_WIDTH, EMPTY_STYLE, ESC, FOREGROUND_CODES, LEVELS, LEVEL_COLORS, LEVEL_SEVERITY, Logger, LoggerManager, Progress, RESET, RESET_CODE, Reporter, SECOND_MS, SEPARATOR_FILL, SEPARATOR_TITLE_GAP, SPINNER_FRAMES, STATUS_COLORS, STATUS_ICONS, STATUS_LEVELS, Spinner, TREE_CHARS, align, cellAt, createANSIRenderer, createCapture, createConsoleSink, createLogger, createLoggerManager, createProgress, createReporter, createSpinner, createStyler, formatArgs, formatDuration, formatRecord, formatTime, isConsoleError, meetsLevel, paint, renderBar, renderBox, renderSeparator, renderTable, renderTree, renderTreeChildren, repeatTo, stringifyValue, strip, stripControls, width, withCapture };
|
|
2116
2116
|
|
|
2117
2117
|
//# sourceMappingURL=index.js.map
|