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