@kubohiroya/turbowarp-runtime-adapter 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - `createTurboWarpDebuggerLog` と `redactDebuggerLogText` を追加。機能拡張の警告とエラーを、エディターのデバッガーアドオンの Logs タブへ書く(コンソールにも出す)。アドオンの log・warn・error ブロックを使うので、VM と GUI は変更しない。書き出せる Logs には URL のクエリー・ローカルのパス・資格情報を伏せて渡す。行はブロックに結び付けない(アドオンのプレビューが多くのブロックで失敗し、一覧が描かれなくなるため)。既存の公開 API の動作は変えない。
6
+
3
7
  ## 0.4.0
4
8
 
5
9
  - Node.js 26・pnpm 11.28.8の検証済みツールチェーンとTrusted Publisherリリースに追従。
package/README.ja.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - 小さく検証された host adapter 経由で hats を起動する。
14
14
  - Stage target、sprite target、renderer、monitor store を読む。
15
15
  - Stage に宣言された exact broadcast name 用の汎用 `broadcastMessageAndWait` port を提供する。
16
+ - 機能拡張の警告とエラーを、エディターのデバッガーの Logs タブへ書く。
16
17
  - extension の `getInfo()` block list のうち定型部分を組み立てる。
17
18
  - Scratch VM への直接アクセスを狭い interface に閉じ込める。
18
19
 
@@ -68,6 +69,24 @@ const surface = build.surface(
68
69
  const written = coerceScalarBlockValue(args.VALUE, args.TYPE, {errorCodePrefix: 'K4'});
69
70
  ```
70
71
 
72
+ ## デバッガーのログ
73
+
74
+ `createTurboWarpDebuggerLog({Scratch, source})` は、機能拡張の警告とエラーを、エディターのデバッガーアドオンの Logs タブへ書く。これを使うすべての機能拡張と、作品自身のログブロックの出力が、同じ場所に並ぶ。各行の先頭には `[source]` を付ける。
75
+
76
+ ```js
77
+ const log = createTurboWarpDebuggerLog({Scratch, source: 'My extension'});
78
+ log.warn('Camera unavailable.', error);
79
+ log.error('Playback failed.', error);
80
+ ```
81
+
82
+ - 書き込み先の runtime は `runtime`、なければ `createTurboWarpRuntimeHost` と同じく `Scratch.vm.runtime` から取る。渡した runtime が runtime でなければ `TWRH-RUNTIME-001` を投げる。どちらもなければコンソールにだけ出す。
83
+ - `log`・`warn`・`error` は、メッセージと省略できる詳細を受け取る。詳細は、`Error` なら `名前: メッセージ [code]`、普通のオブジェクトならおよそ `maxLength` 文字までの JSON にする。デバッガーに届いたかどうかを返す。
84
+ - 行はブロックに結び付けない。アドオンは結び付いたブロックの最初の入力をプレビューするが、自分のログブロック以外の多くのブロック(たとえば数値の入力)ではそのプレビューが失敗し、Logs の一覧全体が描かれなくなる。
85
+ - 同じ内容は常にコンソールにも、手を加えずに出す。詳細は別の引数で渡すので、開発者ツールではスタックも見られる。デバッガーだけに書くときは `console: null` を渡す。
86
+ - Logs タブは内容を書き出せるので、デバッガーへ渡す文は先に `redact` を通す。既定の `redactDebuggerLogText` は、URL を origin とパスだけにし、ローカルのファイルパスを `[local path]` に、`token=`・`password:` などの後ろの値を `[redacted]` に置き換える。`redact: null` ならそのまま渡す。
87
+ - 書き込みにはアドオン自身の log・warn・error ブロック(`runtime.getAddonBlock`)を使う。VM と GUI に新しい口は要らない。アドオンがない環境(プレイヤー、パッケージャー、アドオンを切ったとき)では `available()` が false になり、コンソールにだけ出す。
88
+ - `maxLength` 文字(既定は 1000)を超える行は、文字の途中では切らずに切り詰める。
89
+
71
90
  ## 互換性
72
91
 
73
92
  既存 app が安定した app 固有 diagnostics を公開している場合は、`errorCodePrefix` を渡せます。たとえば `tm-kamishibai` は runtime interaction mechanics をこの package に委譲しつつ、`K4-BROADCAST-*` error を維持できます。
package/README.md CHANGED
@@ -14,6 +14,7 @@ This package intentionally does not define app-specific DSL semantics, block met
14
14
  - Read the Stage target, the sprite targets, the renderer, and the monitor stores.
15
15
  - Provide a generic `broadcastMessageAndWait` port for exact Stage broadcast names.
16
16
  - Build the repetitive parts of an extension `getInfo()` block list.
17
+ - Write extension warnings and errors to the Logs tab of the editor's debugger.
17
18
  - Keep Scratch VM access behind a narrow interface.
18
19
 
19
20
  ## Runtime surface
@@ -68,6 +69,24 @@ const surface = build.surface(
68
69
  const written = coerceScalarBlockValue(args.VALUE, args.TYPE, {errorCodePrefix: 'K4'});
69
70
  ```
70
71
 
72
+ ## Debugger log
73
+
74
+ `createTurboWarpDebuggerLog({Scratch, source})` writes an extension's warnings and errors to the Logs tab of the editor's debugger add-on, where every extension using it and the project's own log blocks show together. Each entry starts with `[source]`.
75
+
76
+ ```js
77
+ const log = createTurboWarpDebuggerLog({Scratch, source: 'My extension'});
78
+ log.warn('Camera unavailable.', error);
79
+ log.error('Playback failed.', error);
80
+ ```
81
+
82
+ - The runtime is `runtime`, or else `Scratch.vm.runtime`, as `createTurboWarpRuntimeHost` resolves it. A runtime that is given must be one (`TWRH-RUNTIME-001` otherwise). Without any, entries only reach the console.
83
+ - `log`, `warn` and `error` take a message, an optional detail (an `Error` becomes `Name: message [code]`, a plain object JSON of at most about `maxLength` characters). Each returns whether the entry reached the debugger.
84
+ - An entry is not linked to a block. The add-on previews the first input of a linked block, and that preview fails on most blocks other than its own log blocks (a number input, for one), which stops the whole Logs list from drawing.
85
+ - The entry always goes to the console too, unchanged and with the detail as a separate argument, so developer tools keep the stack. Pass `console: null` to write only to the debugger.
86
+ - The Logs tab can export its entries, so the debugger's text goes through `redact` first. The default, `redactDebuggerLogText`, keeps only the origin and path of a URL, replaces local file paths with `[local path]` and credentials after `token=`, `password:` and the like with `[redacted]`. `redact: null` keeps the text as it is.
87
+ - Entries go through the add-on's own log, warn and error blocks (`runtime.getAddonBlock`), so the VM and the GUI need no new interface. Without the add-on (a player, the packager, the add-on turned off) `available()` is false and entries only reach the console.
88
+ - An entry longer than `maxLength` characters (1000 by default) is cut between characters, never inside one.
89
+
71
90
  ## Compatibility
72
91
 
73
92
  Consumers that already expose stable app-specific diagnostics can pass `errorCodePrefix`. For example, `tm-kamishibai` can keep `K4-BROADCAST-*` errors while delegating the runtime interaction mechanics to this package.
package/dist/index.d.ts CHANGED
@@ -162,3 +162,66 @@ export type ScalarBlockValueResult = Readonly<{
162
162
  export declare function coerceScalarBlockValue(value: unknown, type: string, options?: {
163
163
  errorCodePrefix?: string;
164
164
  }): ScalarBlockValueResult;
165
+ export type DebuggerLogLevel = 'log' | 'warn' | 'error';
166
+ /** The slice of the runtime the debugger log reaches through: the editor add-ons' blocks. */
167
+ export interface DebuggerLogRuntimeLike {
168
+ getAddonBlock?: (procedureCode: string) => {
169
+ callback(args: Record<string, unknown>, util: {
170
+ thread?: unknown;
171
+ }): unknown;
172
+ } | undefined;
173
+ }
174
+ export interface DebuggerLogConsole {
175
+ log(...data: unknown[]): void;
176
+ warn(...data: unknown[]): void;
177
+ error(...data: unknown[]): void;
178
+ }
179
+ export interface TurboWarpDebuggerLogOptions {
180
+ /** The runtime to write through. Without it, `Scratch.vm.runtime`, as `createTurboWarpRuntimeHost` resolves it. */
181
+ runtime?: unknown;
182
+ Scratch?: ScratchRuntimeHost;
183
+ /** A name put in front of every entry, so entries from several extensions can be told apart. */
184
+ source?: string;
185
+ /**
186
+ * Where an entry also goes. Defaults to the global `console`, so developer tools keep showing
187
+ * everything. `null` writes only to the debugger.
188
+ */
189
+ console?: DebuggerLogConsole | null;
190
+ /** Longest text of one debugger entry, in characters (code points). Defaults to 1000. */
191
+ maxLength?: number;
192
+ /**
193
+ * Applied to the debugger's text, which the Logs tab can export. Defaults to
194
+ * `redactDebuggerLogText`. `null` keeps the text as it is. The console gets the original.
195
+ */
196
+ redact?: ((text: string) => string) | null;
197
+ }
198
+ export interface TurboWarpDebuggerLog {
199
+ /** Whether the editor's debugger add-on is on and takes entries. */
200
+ available(): boolean;
201
+ /**
202
+ * Each returns true when the entry reached the debugger. An entry is not linked to a block: the
203
+ * add-on previews the first input of a linked block and fails on most blocks that are not its
204
+ * own log blocks (a number input, for one), which stops the whole Logs list from drawing.
205
+ */
206
+ log(message: string, detail?: unknown): boolean;
207
+ warn(message: string, detail?: unknown): boolean;
208
+ error(message: string, detail?: unknown): boolean;
209
+ }
210
+ /** The procedure codes of the debugger add-on's log blocks. */
211
+ export declare const debuggerLogProcedureCodes: Readonly<Record<DebuggerLogLevel, string>>;
212
+ /**
213
+ * Hide what should not leave the machine in a log the Logs tab can export: a URL keeps only its
214
+ * origin and path, a local file path becomes `[local path]`, and a credential after `token=`,
215
+ * `password:` and the like becomes `[redacted]`.
216
+ */
217
+ export declare function redactDebuggerLogText(text: string): string;
218
+ /**
219
+ * A log that writes to the Logs tab of the editor's debugger add-on, so the warnings and errors of
220
+ * every extension show in one place next to the project's own log blocks.
221
+ *
222
+ * Entries go through the add-on's own log, warn and error blocks (`runtime.getAddonBlock`), so
223
+ * neither the VM nor the GUI needs a new interface. Without the add-on (a player, the packager,
224
+ * plain TurboWarp with the add-on off) or without a runtime, an entry only goes to the console.
225
+ * A runtime that is given must be one: anything else throws `TWRH-RUNTIME-001`.
226
+ */
227
+ export declare function createTurboWarpDebuggerLog(options?: TurboWarpDebuggerLogOptions): TurboWarpDebuggerLog;
package/dist/index.js CHANGED
@@ -498,3 +498,131 @@ export function coerceScalarBlockValue(value, type, options = {}) {
498
498
  }
499
499
  return Object.freeze({ ok: false, code: prefixedCode(prefix, 'VARIABLE-WRITE-TYPE') });
500
500
  }
501
+ // The add-on names its blocks with zero-width spaces so that they cannot clash with a project's own.
502
+ const zeroWidthSpace = '\u200B\u200B';
503
+ /** The procedure codes of the debugger add-on's log blocks. */
504
+ export const debuggerLogProcedureCodes = Object.freeze({
505
+ log: `${zeroWidthSpace}log${zeroWidthSpace} %s`,
506
+ warn: `${zeroWidthSpace}warn${zeroWidthSpace} %s`,
507
+ error: `${zeroWidthSpace}error${zeroWidthSpace} %s`,
508
+ });
509
+ /**
510
+ * Hide what should not leave the machine in a log the Logs tab can export: a URL keeps only its
511
+ * origin and path, a local file path becomes `[local path]`, and a credential after `token=`,
512
+ * `password:` and the like becomes `[redacted]`.
513
+ */
514
+ export function redactDebuggerLogText(text) {
515
+ return text
516
+ .replace(/https?:\/\/[^\s<>"']+/gu, (address) => {
517
+ try {
518
+ const url = new URL(address);
519
+ return `${url.origin}${url.pathname}`;
520
+ }
521
+ catch {
522
+ return '[URL]';
523
+ }
524
+ })
525
+ .replace(/(?:file:\/\/|[A-Za-z]:\\|\/(?:Users|home|private|tmp|var|Volumes)\/)[^\s"'<>]*/gu, '[local path]')
526
+ .replace(/\b(token|password|secret|authorization|api[_-]?key)\s*[:=]\s*[^\s,;]+/giu, '$1=[redacted]');
527
+ }
528
+ // JSON of at most about `budget` characters. Values past the budget are left out, so a large
529
+ // record is not turned into a long string only to be cut.
530
+ function boundedJson(detail, budget) {
531
+ let used = 0;
532
+ let cut = false;
533
+ try {
534
+ const json = JSON.stringify(detail, (key, value) => {
535
+ if (used > budget) {
536
+ cut = true;
537
+ return undefined;
538
+ }
539
+ used += key.length + (typeof value === 'string' ? value.length : 8);
540
+ return value;
541
+ });
542
+ return json === undefined ? '' : `${json}${cut ? '…' : ''}`;
543
+ }
544
+ catch {
545
+ return String(detail);
546
+ }
547
+ }
548
+ function detailText(detail, budget) {
549
+ if (detail === undefined || detail === null)
550
+ return '';
551
+ if (typeof detail === 'string')
552
+ return detail;
553
+ if (typeof detail === 'number' || typeof detail === 'boolean')
554
+ return String(detail);
555
+ const record = detail;
556
+ if (typeof record.message === 'string' || detail instanceof Error) {
557
+ const name = typeof record.name === 'string' && record.name !== 'Error' ? record.name : '';
558
+ const message = String(record.message ?? '');
559
+ const code = typeof record.code === 'string' && record.code ? ` [${record.code}]` : '';
560
+ return `${name && message ? `${name}: ` : name}${message}${code}`;
561
+ }
562
+ return boundedJson(detail, budget);
563
+ }
564
+ function shorten(text, maxLength) {
565
+ if (text.length <= maxLength)
566
+ return text;
567
+ // Count code points, so a cut never leaves half of a surrogate pair.
568
+ const characters = Array.from(text);
569
+ if (characters.length <= maxLength)
570
+ return text;
571
+ return `${characters.slice(0, Math.max(0, maxLength - 1)).join('')}…`;
572
+ }
573
+ /**
574
+ * A log that writes to the Logs tab of the editor's debugger add-on, so the warnings and errors of
575
+ * every extension show in one place next to the project's own log blocks.
576
+ *
577
+ * Entries go through the add-on's own log, warn and error blocks (`runtime.getAddonBlock`), so
578
+ * neither the VM nor the GUI needs a new interface. Without the add-on (a player, the packager,
579
+ * plain TurboWarp with the add-on off) or without a runtime, an entry only goes to the console.
580
+ * A runtime that is given must be one: anything else throws `TWRH-RUNTIME-001`.
581
+ */
582
+ export function createTurboWarpDebuggerLog(options = {}) {
583
+ const candidate = options.runtime ?? options.Scratch?.vm?.runtime;
584
+ const runtime = candidate === undefined || candidate === null
585
+ ? null
586
+ : validateRuntime(candidate);
587
+ const output = options.console === undefined ? console : options.console;
588
+ const prefix = options.source ? `[${options.source}] ` : '';
589
+ const maxLength = options.maxLength ?? 1000;
590
+ const redact = options.redact === undefined ? redactDebuggerLogText : options.redact;
591
+ const block = (level) => {
592
+ try {
593
+ return runtime?.getAddonBlock?.(debuggerLogProcedureCodes[level]);
594
+ }
595
+ catch {
596
+ return undefined;
597
+ }
598
+ };
599
+ const write = (level, message, detail) => {
600
+ if (output) {
601
+ if (detail === undefined)
602
+ output[level](`${prefix}${message}`);
603
+ else
604
+ output[level](`${prefix}${message}`, detail);
605
+ }
606
+ const logBlock = block(level);
607
+ if (!logBlock)
608
+ return false;
609
+ try {
610
+ const detailPart = detailText(detail, maxLength);
611
+ let text = `${prefix}${message}${detailPart ? ` ${detailPart}` : ''}`;
612
+ if (redact)
613
+ text = redact(text);
614
+ logBlock.callback({ content: shorten(text, maxLength) }, { thread: undefined });
615
+ return true;
616
+ }
617
+ catch {
618
+ // The debugger cannot take the entry. The console already has it.
619
+ return false;
620
+ }
621
+ };
622
+ return Object.freeze({
623
+ available: () => Boolean(block('log')),
624
+ log: (message, detail) => write('log', message, detail),
625
+ warn: (message, detail) => write('warn', message, detail),
626
+ error: (message, detail) => write('error', message, detail),
627
+ });
628
+ }
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-runtime-adapter",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "App-neutral adapters for interacting with the TurboWarp runtime.",
5
- "author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
5
+ "author": {
6
+ "name": "Hiroya Kubo",
7
+ "email": "hiroya@cuc.ac.jp",
8
+ "url": "https://github.com/kubohiroya"
9
+ },
6
10
  "license": "MPL-2.0",
7
11
  "type": "module",
8
12
  "sideEffects": false,
package/src/index.ts CHANGED
@@ -743,3 +743,184 @@ export function coerceScalarBlockValue(
743
743
  }
744
744
  return Object.freeze({ok: false as const, code: prefixedCode(prefix, 'VARIABLE-WRITE-TYPE')});
745
745
  }
746
+
747
+ export type DebuggerLogLevel = 'log' | 'warn' | 'error';
748
+
749
+ /** The slice of the runtime the debugger log reaches through: the editor add-ons' blocks. */
750
+ export interface DebuggerLogRuntimeLike {
751
+ getAddonBlock?: (
752
+ procedureCode: string,
753
+ ) => {callback(args: Record<string, unknown>, util: {thread?: unknown}): unknown} | undefined;
754
+ }
755
+
756
+ export interface DebuggerLogConsole {
757
+ log(...data: unknown[]): void;
758
+ warn(...data: unknown[]): void;
759
+ error(...data: unknown[]): void;
760
+ }
761
+
762
+ export interface TurboWarpDebuggerLogOptions {
763
+ /** The runtime to write through. Without it, `Scratch.vm.runtime`, as `createTurboWarpRuntimeHost` resolves it. */
764
+ runtime?: unknown;
765
+ Scratch?: ScratchRuntimeHost;
766
+ /** A name put in front of every entry, so entries from several extensions can be told apart. */
767
+ source?: string;
768
+ /**
769
+ * Where an entry also goes. Defaults to the global `console`, so developer tools keep showing
770
+ * everything. `null` writes only to the debugger.
771
+ */
772
+ console?: DebuggerLogConsole | null;
773
+ /** Longest text of one debugger entry, in characters (code points). Defaults to 1000. */
774
+ maxLength?: number;
775
+ /**
776
+ * Applied to the debugger's text, which the Logs tab can export. Defaults to
777
+ * `redactDebuggerLogText`. `null` keeps the text as it is. The console gets the original.
778
+ */
779
+ redact?: ((text: string) => string) | null;
780
+ }
781
+
782
+ export interface TurboWarpDebuggerLog {
783
+ /** Whether the editor's debugger add-on is on and takes entries. */
784
+ available(): boolean;
785
+ /**
786
+ * Each returns true when the entry reached the debugger. An entry is not linked to a block: the
787
+ * add-on previews the first input of a linked block and fails on most blocks that are not its
788
+ * own log blocks (a number input, for one), which stops the whole Logs list from drawing.
789
+ */
790
+ log(message: string, detail?: unknown): boolean;
791
+ warn(message: string, detail?: unknown): boolean;
792
+ error(message: string, detail?: unknown): boolean;
793
+ }
794
+
795
+ // The add-on names its blocks with zero-width spaces so that they cannot clash with a project's own.
796
+ const zeroWidthSpace = '\u200B\u200B';
797
+
798
+ /** The procedure codes of the debugger add-on's log blocks. */
799
+ export const debuggerLogProcedureCodes: Readonly<Record<DebuggerLogLevel, string>> = Object.freeze({
800
+ log: `${zeroWidthSpace}log${zeroWidthSpace} %s`,
801
+ warn: `${zeroWidthSpace}warn${zeroWidthSpace} %s`,
802
+ error: `${zeroWidthSpace}error${zeroWidthSpace} %s`,
803
+ });
804
+
805
+ /**
806
+ * Hide what should not leave the machine in a log the Logs tab can export: a URL keeps only its
807
+ * origin and path, a local file path becomes `[local path]`, and a credential after `token=`,
808
+ * `password:` and the like becomes `[redacted]`.
809
+ */
810
+ export function redactDebuggerLogText(text: string): string {
811
+ return text
812
+ .replace(/https?:\/\/[^\s<>"']+/gu, (address) => {
813
+ try {
814
+ const url = new URL(address);
815
+ return `${url.origin}${url.pathname}`;
816
+ } catch {
817
+ return '[URL]';
818
+ }
819
+ })
820
+ .replace(
821
+ /(?:file:\/\/|[A-Za-z]:\\|\/(?:Users|home|private|tmp|var|Volumes)\/)[^\s"'<>]*/gu,
822
+ '[local path]',
823
+ )
824
+ .replace(
825
+ /\b(token|password|secret|authorization|api[_-]?key)\s*[:=]\s*[^\s,;]+/giu,
826
+ '$1=[redacted]',
827
+ );
828
+ }
829
+
830
+ // JSON of at most about `budget` characters. Values past the budget are left out, so a large
831
+ // record is not turned into a long string only to be cut.
832
+ function boundedJson(detail: unknown, budget: number): string {
833
+ let used = 0;
834
+ let cut = false;
835
+ try {
836
+ const json = JSON.stringify(detail, (key: string, value: unknown) => {
837
+ if (used > budget) {
838
+ cut = true;
839
+ return undefined;
840
+ }
841
+ used += key.length + (typeof value === 'string' ? value.length : 8);
842
+ return value;
843
+ });
844
+ return json === undefined ? '' : `${json}${cut ? '…' : ''}`;
845
+ } catch {
846
+ return String(detail);
847
+ }
848
+ }
849
+
850
+ function detailText(detail: unknown, budget: number): string {
851
+ if (detail === undefined || detail === null) return '';
852
+ if (typeof detail === 'string') return detail;
853
+ if (typeof detail === 'number' || typeof detail === 'boolean') return String(detail);
854
+ const record = detail as {name?: unknown; code?: unknown; message?: unknown};
855
+ if (typeof record.message === 'string' || detail instanceof Error) {
856
+ const name = typeof record.name === 'string' && record.name !== 'Error' ? record.name : '';
857
+ const message = String(record.message ?? '');
858
+ const code = typeof record.code === 'string' && record.code ? ` [${record.code}]` : '';
859
+ return `${name && message ? `${name}: ` : name}${message}${code}`;
860
+ }
861
+ return boundedJson(detail, budget);
862
+ }
863
+
864
+ function shorten(text: string, maxLength: number) {
865
+ if (text.length <= maxLength) return text;
866
+ // Count code points, so a cut never leaves half of a surrogate pair.
867
+ const characters = Array.from(text);
868
+ if (characters.length <= maxLength) return text;
869
+ return `${characters.slice(0, Math.max(0, maxLength - 1)).join('')}…`;
870
+ }
871
+
872
+ /**
873
+ * A log that writes to the Logs tab of the editor's debugger add-on, so the warnings and errors of
874
+ * every extension show in one place next to the project's own log blocks.
875
+ *
876
+ * Entries go through the add-on's own log, warn and error blocks (`runtime.getAddonBlock`), so
877
+ * neither the VM nor the GUI needs a new interface. Without the add-on (a player, the packager,
878
+ * plain TurboWarp with the add-on off) or without a runtime, an entry only goes to the console.
879
+ * A runtime that is given must be one: anything else throws `TWRH-RUNTIME-001`.
880
+ */
881
+ export function createTurboWarpDebuggerLog(
882
+ options: TurboWarpDebuggerLogOptions = {},
883
+ ): TurboWarpDebuggerLog {
884
+ const candidate = options.runtime ?? options.Scratch?.vm?.runtime;
885
+ const runtime =
886
+ candidate === undefined || candidate === null
887
+ ? null
888
+ : (validateRuntime(candidate) as unknown as DebuggerLogRuntimeLike);
889
+ const output = options.console === undefined ? console : options.console;
890
+ const prefix = options.source ? `[${options.source}] ` : '';
891
+ const maxLength = options.maxLength ?? 1000;
892
+ const redact = options.redact === undefined ? redactDebuggerLogText : options.redact;
893
+ const block = (level: DebuggerLogLevel) => {
894
+ try {
895
+ return runtime?.getAddonBlock?.(debuggerLogProcedureCodes[level]);
896
+ } catch {
897
+ return undefined;
898
+ }
899
+ };
900
+
901
+ const write = (level: DebuggerLogLevel, message: string, detail: unknown) => {
902
+ if (output) {
903
+ if (detail === undefined) output[level](`${prefix}${message}`);
904
+ else output[level](`${prefix}${message}`, detail);
905
+ }
906
+ const logBlock = block(level);
907
+ if (!logBlock) return false;
908
+ try {
909
+ const detailPart = detailText(detail, maxLength);
910
+ let text = `${prefix}${message}${detailPart ? ` ${detailPart}` : ''}`;
911
+ if (redact) text = redact(text);
912
+ logBlock.callback({content: shorten(text, maxLength)}, {thread: undefined});
913
+ return true;
914
+ } catch {
915
+ // The debugger cannot take the entry. The console already has it.
916
+ return false;
917
+ }
918
+ };
919
+
920
+ return Object.freeze({
921
+ available: () => Boolean(block('log')),
922
+ log: (message: string, detail?: unknown) => write('log', message, detail),
923
+ warn: (message: string, detail?: unknown) => write('warn', message, detail),
924
+ error: (message: string, detail?: unknown) => write('error', message, detail),
925
+ });
926
+ }