@buoy-gg/console 4.0.2
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/LICENSE +58 -0
- package/README.md +55 -0
- package/lib/commonjs/components/ConsoleIcon.js +47 -0
- package/lib/commonjs/components/ConsoleIcon.js.map +1 -0
- package/lib/commonjs/components/ConsoleLogList.js +145 -0
- package/lib/commonjs/components/ConsoleLogList.js.map +1 -0
- package/lib/commonjs/components/ConsoleModal.js +48 -0
- package/lib/commonjs/components/ConsoleModal.js.map +1 -0
- package/lib/commonjs/components/ConsoleRoot.js +28 -0
- package/lib/commonjs/components/ConsoleRoot.js.map +1 -0
- package/lib/commonjs/devtools/ConsoleBody.js +235 -0
- package/lib/commonjs/devtools/ConsoleBody.js.map +1 -0
- package/lib/commonjs/devtools/ConsoleHeader.js +146 -0
- package/lib/commonjs/devtools/ConsoleHeader.js.map +1 -0
- package/lib/commonjs/devtools/ConsoleMessageRow.js +338 -0
- package/lib/commonjs/devtools/ConsoleMessageRow.js.map +1 -0
- package/lib/commonjs/devtools/ConsoleValue.js +254 -0
- package/lib/commonjs/devtools/ConsoleValue.js.map +1 -0
- package/lib/commonjs/devtools/ConsoleView.js +51 -0
- package/lib/commonjs/devtools/ConsoleView.js.map +1 -0
- package/lib/commonjs/devtools/consoleFilter.js +125 -0
- package/lib/commonjs/devtools/consoleFilter.js.map +1 -0
- package/lib/commonjs/devtools/consoleFormat.js +313 -0
- package/lib/commonjs/devtools/consoleFormat.js.map +1 -0
- package/lib/commonjs/devtools/origin.js +191 -0
- package/lib/commonjs/devtools/origin.js.map +1 -0
- package/lib/commonjs/devtools/theme.js +143 -0
- package/lib/commonjs/devtools/theme.js.map +1 -0
- package/lib/commonjs/devtools/useConsoleController.js +82 -0
- package/lib/commonjs/devtools/useConsoleController.js.map +1 -0
- package/lib/commonjs/devtools/useConsoleMessages.js +241 -0
- package/lib/commonjs/devtools/useConsoleMessages.js.map +1 -0
- package/lib/commonjs/hooks/useConsoleLogs.js +23 -0
- package/lib/commonjs/hooks/useConsoleLogs.js.map +1 -0
- package/lib/commonjs/index.js +175 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +45 -0
- package/lib/commonjs/preset.js.map +1 -0
- package/lib/commonjs/store/consoleLogStore.js +298 -0
- package/lib/commonjs/store/consoleLogStore.js.map +1 -0
- package/lib/commonjs/store/sanitize.js +64 -0
- package/lib/commonjs/store/sanitize.js.map +1 -0
- package/lib/commonjs/sync/consoleSyncAdapter.js +47 -0
- package/lib/commonjs/sync/consoleSyncAdapter.js.map +1 -0
- package/lib/module/components/ConsoleIcon.js +42 -0
- package/lib/module/components/ConsoleIcon.js.map +1 -0
- package/lib/module/components/ConsoleLogList.js +140 -0
- package/lib/module/components/ConsoleLogList.js.map +1 -0
- package/lib/module/components/ConsoleModal.js +44 -0
- package/lib/module/components/ConsoleModal.js.map +1 -0
- package/lib/module/components/ConsoleRoot.js +25 -0
- package/lib/module/components/ConsoleRoot.js.map +1 -0
- package/lib/module/devtools/ConsoleBody.js +230 -0
- package/lib/module/devtools/ConsoleBody.js.map +1 -0
- package/lib/module/devtools/ConsoleHeader.js +141 -0
- package/lib/module/devtools/ConsoleHeader.js.map +1 -0
- package/lib/module/devtools/ConsoleMessageRow.js +333 -0
- package/lib/module/devtools/ConsoleMessageRow.js.map +1 -0
- package/lib/module/devtools/ConsoleValue.js +249 -0
- package/lib/module/devtools/ConsoleValue.js.map +1 -0
- package/lib/module/devtools/ConsoleView.js +46 -0
- package/lib/module/devtools/ConsoleView.js.map +1 -0
- package/lib/module/devtools/consoleFilter.js +119 -0
- package/lib/module/devtools/consoleFilter.js.map +1 -0
- package/lib/module/devtools/consoleFormat.js +309 -0
- package/lib/module/devtools/consoleFormat.js.map +1 -0
- package/lib/module/devtools/origin.js +183 -0
- package/lib/module/devtools/origin.js.map +1 -0
- package/lib/module/devtools/theme.js +140 -0
- package/lib/module/devtools/theme.js.map +1 -0
- package/lib/module/devtools/useConsoleController.js +79 -0
- package/lib/module/devtools/useConsoleController.js.map +1 -0
- package/lib/module/devtools/useConsoleMessages.js +238 -0
- package/lib/module/devtools/useConsoleMessages.js.map +1 -0
- package/lib/module/hooks/useConsoleLogs.js +19 -0
- package/lib/module/hooks/useConsoleLogs.js.map +1 -0
- package/lib/module/index.js +52 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/preset.js +40 -0
- package/lib/module/preset.js.map +1 -0
- package/lib/module/store/consoleLogStore.js +284 -0
- package/lib/module/store/consoleLogStore.js.map +1 -0
- package/lib/module/store/sanitize.js +60 -0
- package/lib/module/store/sanitize.js.map +1 -0
- package/lib/module/sync/consoleSyncAdapter.js +43 -0
- package/lib/module/sync/consoleSyncAdapter.js.map +1 -0
- package/lib/typescript/components/ConsoleIcon.d.ts +14 -0
- package/lib/typescript/components/ConsoleIcon.d.ts.map +1 -0
- package/lib/typescript/components/ConsoleLogList.d.ts +8 -0
- package/lib/typescript/components/ConsoleLogList.d.ts.map +1 -0
- package/lib/typescript/components/ConsoleModal.d.ts +18 -0
- package/lib/typescript/components/ConsoleModal.d.ts.map +1 -0
- package/lib/typescript/components/ConsoleRoot.d.ts +14 -0
- package/lib/typescript/components/ConsoleRoot.d.ts.map +1 -0
- package/lib/typescript/devtools/ConsoleBody.d.ts +11 -0
- package/lib/typescript/devtools/ConsoleBody.d.ts.map +1 -0
- package/lib/typescript/devtools/ConsoleHeader.d.ts +17 -0
- package/lib/typescript/devtools/ConsoleHeader.d.ts.map +1 -0
- package/lib/typescript/devtools/ConsoleMessageRow.d.ts +20 -0
- package/lib/typescript/devtools/ConsoleMessageRow.d.ts.map +1 -0
- package/lib/typescript/devtools/ConsoleValue.d.ts +21 -0
- package/lib/typescript/devtools/ConsoleValue.d.ts.map +1 -0
- package/lib/typescript/devtools/ConsoleView.d.ts +10 -0
- package/lib/typescript/devtools/ConsoleView.d.ts.map +1 -0
- package/lib/typescript/devtools/consoleFilter.d.ts +31 -0
- package/lib/typescript/devtools/consoleFilter.d.ts.map +1 -0
- package/lib/typescript/devtools/consoleFormat.d.ts +29 -0
- package/lib/typescript/devtools/consoleFormat.d.ts.map +1 -0
- package/lib/typescript/devtools/origin.d.ts +39 -0
- package/lib/typescript/devtools/origin.d.ts.map +1 -0
- package/lib/typescript/devtools/theme.d.ts +57 -0
- package/lib/typescript/devtools/theme.d.ts.map +1 -0
- package/lib/typescript/devtools/useConsoleController.d.ts +35 -0
- package/lib/typescript/devtools/useConsoleController.d.ts.map +1 -0
- package/lib/typescript/devtools/useConsoleMessages.d.ts +74 -0
- package/lib/typescript/devtools/useConsoleMessages.d.ts.map +1 -0
- package/lib/typescript/hooks/useConsoleLogs.d.ts +11 -0
- package/lib/typescript/hooks/useConsoleLogs.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +26 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +38 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/store/consoleLogStore.d.ts +71 -0
- package/lib/typescript/store/consoleLogStore.d.ts.map +1 -0
- package/lib/typescript/store/sanitize.d.ts +11 -0
- package/lib/typescript/store/sanitize.d.ts.map +1 -0
- package/lib/typescript/sync/consoleSyncAdapter.d.ts +30 -0
- package/lib/typescript/sync/consoleSyncAdapter.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/components/ConsoleIcon.tsx +49 -0
- package/src/components/ConsoleLogList.tsx +133 -0
- package/src/components/ConsoleModal.tsx +48 -0
- package/src/components/ConsoleRoot.tsx +25 -0
- package/src/devtools/ConsoleBody.tsx +225 -0
- package/src/devtools/ConsoleHeader.tsx +149 -0
- package/src/devtools/ConsoleMessageRow.tsx +345 -0
- package/src/devtools/ConsoleValue.tsx +265 -0
- package/src/devtools/ConsoleView.tsx +41 -0
- package/src/devtools/consoleFilter.ts +123 -0
- package/src/devtools/consoleFormat.ts +314 -0
- package/src/devtools/origin.ts +190 -0
- package/src/devtools/theme.ts +137 -0
- package/src/devtools/useConsoleController.ts +104 -0
- package/src/devtools/useConsoleMessages.ts +337 -0
- package/src/hooks/useConsoleLogs.ts +26 -0
- package/src/index.tsx +69 -0
- package/src/preset.tsx +35 -0
- package/src/store/consoleLogStore.ts +346 -0
- package/src/store/sanitize.ts +58 -0
- package/src/sync/consoleSyncAdapter.ts +49 -0
package/src/preset.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-configured Console tool for FloatingDevTools.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { consoleToolPreset } from '@buoy-gg/console';
|
|
7
|
+
*
|
|
8
|
+
* <FloatingDevTools apps={[consoleToolPreset]} />
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ConsoleIcon, CONSOLE_ICON_COLOR } from "./components/ConsoleIcon";
|
|
13
|
+
import { ConsoleModal } from "./components/ConsoleModal";
|
|
14
|
+
|
|
15
|
+
export const consoleToolPreset = {
|
|
16
|
+
id: "console",
|
|
17
|
+
name: "CONSOLE",
|
|
18
|
+
description: "Captured console.* output",
|
|
19
|
+
slot: "both" as const,
|
|
20
|
+
icon: ({ size, color }: { size: number; color?: string }) => (
|
|
21
|
+
<ConsoleIcon size={size} color={color || CONSOLE_ICON_COLOR} />
|
|
22
|
+
),
|
|
23
|
+
component: ConsoleModal,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Create a custom Console tool configuration. */
|
|
27
|
+
export function createConsoleTool(
|
|
28
|
+
options: { name?: string; description?: string } = {}
|
|
29
|
+
) {
|
|
30
|
+
return {
|
|
31
|
+
...consoleToolPreset,
|
|
32
|
+
name: options.name ?? consoleToolPreset.name,
|
|
33
|
+
description: options.description ?? consoleToolPreset.description,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consoleLogStore
|
|
3
|
+
*
|
|
4
|
+
* Intercepts the global `console` methods and keeps a bounded ring buffer of
|
|
5
|
+
* captured entries, modeled after Chrome DevTools' ConsoleMessage. Direct
|
|
6
|
+
* console patching is the capture path (works identically on Expo, RN CLI, and
|
|
7
|
+
* web). The DevTools-style view layer (filtering, formatting, grouping) is built
|
|
8
|
+
* on top of these raw entries.
|
|
9
|
+
*
|
|
10
|
+
* By default the buffer is in-memory only, so a JS reload starts fresh — just
|
|
11
|
+
* like the Metro/Chrome console. A "Preserve log" toggle persists the buffer to
|
|
12
|
+
* storage and restores it on the next launch (Chrome's "Preserve log").
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
import { persistentStorage } from "@buoy-gg/shared-ui";
|
|
17
|
+
import { sanitizeArgs } from "./sanitize";
|
|
18
|
+
import { parseOrigin, type LogOrigin } from "../devtools/origin";
|
|
19
|
+
|
|
20
|
+
/** The console method that produced an entry. */
|
|
21
|
+
export type ConsoleMethod =
|
|
22
|
+
| "log"
|
|
23
|
+
| "info"
|
|
24
|
+
| "warn"
|
|
25
|
+
| "error"
|
|
26
|
+
| "debug"
|
|
27
|
+
| "trace"
|
|
28
|
+
| "dir"
|
|
29
|
+
| "table"
|
|
30
|
+
| "assert"
|
|
31
|
+
| "group"
|
|
32
|
+
| "groupCollapsed"
|
|
33
|
+
| "groupEnd";
|
|
34
|
+
|
|
35
|
+
/** DevTools log levels (Protocol.Log.LogEntryLevel). */
|
|
36
|
+
export type ConsoleLevel = "verbose" | "info" | "warning" | "error";
|
|
37
|
+
|
|
38
|
+
export interface ConsoleLogEntry {
|
|
39
|
+
/** Monotonic id, unique per captured entry. */
|
|
40
|
+
id: number;
|
|
41
|
+
/** The console method that produced this entry. */
|
|
42
|
+
method: ConsoleMethod;
|
|
43
|
+
/** DevTools level, derived from the method. */
|
|
44
|
+
level: ConsoleLevel;
|
|
45
|
+
/** The raw arguments passed to the console method. */
|
|
46
|
+
args: unknown[];
|
|
47
|
+
/** Pre-rendered single-line text (used for filtering + the simple list). */
|
|
48
|
+
message: string;
|
|
49
|
+
/** Capture time in ms since epoch. */
|
|
50
|
+
timestamp: number;
|
|
51
|
+
/** Captured JS stack (error/warn/trace only), newest frame first. */
|
|
52
|
+
stack?: string;
|
|
53
|
+
/** Where the log came from (component / function / call site). */
|
|
54
|
+
origin?: LogOrigin;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Methods we intercept, in the order we install them. */
|
|
58
|
+
const METHODS: ConsoleMethod[] = [
|
|
59
|
+
"log",
|
|
60
|
+
"info",
|
|
61
|
+
"warn",
|
|
62
|
+
"error",
|
|
63
|
+
"debug",
|
|
64
|
+
"trace",
|
|
65
|
+
"dir",
|
|
66
|
+
"table",
|
|
67
|
+
"assert",
|
|
68
|
+
"group",
|
|
69
|
+
"groupCollapsed",
|
|
70
|
+
"groupEnd",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/** Methods whose call site we capture a stack for. */
|
|
74
|
+
const STACK_METHODS = new Set<ConsoleMethod>(["error", "warn", "trace", "assert"]);
|
|
75
|
+
|
|
76
|
+
/** method → DevTools level. */
|
|
77
|
+
const METHOD_LEVEL: Record<ConsoleMethod, ConsoleLevel> = {
|
|
78
|
+
error: "error",
|
|
79
|
+
assert: "error",
|
|
80
|
+
warn: "warning",
|
|
81
|
+
debug: "verbose",
|
|
82
|
+
log: "info",
|
|
83
|
+
info: "info",
|
|
84
|
+
trace: "info",
|
|
85
|
+
dir: "info",
|
|
86
|
+
table: "info",
|
|
87
|
+
group: "info",
|
|
88
|
+
groupCollapsed: "info",
|
|
89
|
+
groupEnd: "info",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Max entries retained before the oldest are dropped. */
|
|
93
|
+
const MAX_ENTRIES = 1000;
|
|
94
|
+
|
|
95
|
+
type Listener = () => void;
|
|
96
|
+
|
|
97
|
+
let entries: ConsoleLogEntry[] = [];
|
|
98
|
+
let nextId = 1;
|
|
99
|
+
let installed = false;
|
|
100
|
+
|
|
101
|
+
const listeners = new Set<Listener>();
|
|
102
|
+
const clearListeners = new Set<Listener>();
|
|
103
|
+
|
|
104
|
+
/** Saved originals so we can restore on uninstall and still print through. */
|
|
105
|
+
const originals: Partial<Record<ConsoleMethod, (...args: unknown[]) => void>> = {};
|
|
106
|
+
|
|
107
|
+
function emit(): void {
|
|
108
|
+
for (const listener of listeners) listener();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Best-effort, single-line serialization of a single console argument. */
|
|
112
|
+
function formatArg(arg: unknown): string {
|
|
113
|
+
if (typeof arg === "string") return arg;
|
|
114
|
+
if (arg instanceof Error) return arg.stack ?? `${arg.name}: ${arg.message}`;
|
|
115
|
+
if (arg === undefined) return "undefined";
|
|
116
|
+
if (arg === null) return "null";
|
|
117
|
+
if (typeof arg === "function") return `ƒ ${arg.name || "(anonymous)"}`;
|
|
118
|
+
try {
|
|
119
|
+
return JSON.stringify(arg);
|
|
120
|
+
} catch {
|
|
121
|
+
return String(arg);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatArgs(args: unknown[]): string {
|
|
126
|
+
return args.map(formatArg).join(" ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Drop the "Error" header + the interceptor's own frames from a raw stack. */
|
|
130
|
+
function cleanStack(raw?: string): string | undefined {
|
|
131
|
+
if (!raw) return undefined;
|
|
132
|
+
return raw.split("\n").slice(3).join("\n") || undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Nearest owning React component stack (dev only; empty off-render). */
|
|
136
|
+
function captureOwnerStack(): string | undefined {
|
|
137
|
+
try {
|
|
138
|
+
const fn = (React as unknown as { captureOwnerStack?: () => string | null })
|
|
139
|
+
.captureOwnerStack;
|
|
140
|
+
return typeof fn === "function" ? fn() ?? undefined : undefined;
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function record(method: ConsoleMethod, args: unknown[]): void {
|
|
147
|
+
// Capture the call site for every log so we can attribute its origin.
|
|
148
|
+
const rawStack = new Error().stack;
|
|
149
|
+
const origin = parseOrigin(rawStack, captureOwnerStack());
|
|
150
|
+
const entry: ConsoleLogEntry = {
|
|
151
|
+
id: nextId++,
|
|
152
|
+
method,
|
|
153
|
+
level: METHOD_LEVEL[method],
|
|
154
|
+
args,
|
|
155
|
+
message: formatArgs(args),
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
stack: STACK_METHODS.has(method) ? cleanStack(rawStack) : undefined,
|
|
158
|
+
origin,
|
|
159
|
+
};
|
|
160
|
+
entries =
|
|
161
|
+
entries.length >= MAX_ENTRIES
|
|
162
|
+
? [...entries.slice(entries.length - MAX_ENTRIES + 1), entry]
|
|
163
|
+
: [...entries, entry];
|
|
164
|
+
emit();
|
|
165
|
+
scheduleSave();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Patch the global console methods. Idempotent — safe to call more than once.
|
|
170
|
+
* Each patched method still forwards to the original so normal logging keeps
|
|
171
|
+
* working (Metro terminal, Chrome, native logcat/Xcode).
|
|
172
|
+
*/
|
|
173
|
+
export function installConsoleCapture(): void {
|
|
174
|
+
if (installed) return;
|
|
175
|
+
installed = true;
|
|
176
|
+
|
|
177
|
+
for (const method of METHODS) {
|
|
178
|
+
const original = console[method] as ((...args: unknown[]) => void) | undefined;
|
|
179
|
+
if (typeof original !== "function") continue;
|
|
180
|
+
originals[method] = original;
|
|
181
|
+
// Named so origin parsing can identify (and skip) this frame by function
|
|
182
|
+
// name — in a Metro bundle every frame shares one bundle URL, so filename
|
|
183
|
+
// filtering can't distinguish our wrapper from the real caller.
|
|
184
|
+
console[method] = function buoyConsoleProxy(...args: unknown[]) {
|
|
185
|
+
record(method, args);
|
|
186
|
+
original.apply(console, args);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Restore the original console methods. */
|
|
192
|
+
export function uninstallConsoleCapture(): void {
|
|
193
|
+
if (!installed) return;
|
|
194
|
+
installed = false;
|
|
195
|
+
for (const method of METHODS) {
|
|
196
|
+
const original = originals[method];
|
|
197
|
+
if (original) console[method] = original;
|
|
198
|
+
delete originals[method];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Subscribe to changes. Returns an unsubscribe function. */
|
|
203
|
+
export function subscribe(listener: Listener): () => void {
|
|
204
|
+
listeners.add(listener);
|
|
205
|
+
return () => {
|
|
206
|
+
listeners.delete(listener);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Current snapshot (stable reference between mutations for useSyncExternalStore). */
|
|
211
|
+
export function getEntries(): ConsoleLogEntry[] {
|
|
212
|
+
return entries;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Clear all captured entries. Fires onClear listeners (e.g. to forward the
|
|
216
|
+
* clear to a remote device when running in mirror mode). */
|
|
217
|
+
export function clearEntries(): void {
|
|
218
|
+
for (const listener of clearListeners) listener();
|
|
219
|
+
void clearSavedBuffer();
|
|
220
|
+
if (entries.length === 0) return;
|
|
221
|
+
entries = [];
|
|
222
|
+
emit();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Remote-mirror mode (desktop dashboard) ──────────────────────────────────
|
|
226
|
+
// The desktop imports this same store but, instead of patching its own console,
|
|
227
|
+
// drives it from device snapshots via replaceEntries(). Capture is never
|
|
228
|
+
// installed there, so local and remote modes never mix.
|
|
229
|
+
|
|
230
|
+
/** Replace the whole entry list from a remote snapshot. */
|
|
231
|
+
export function replaceEntries(next: ConsoleLogEntry[]): void {
|
|
232
|
+
entries = next.length > MAX_ENTRIES ? next.slice(next.length - MAX_ENTRIES) : next;
|
|
233
|
+
emit();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Register a listener fired when clearEntries() is called. Returns an
|
|
237
|
+
* unsubscribe function. Used by the dashboard to forward clears to the device. */
|
|
238
|
+
export function onClear(listener: Listener): () => void {
|
|
239
|
+
clearListeners.add(listener);
|
|
240
|
+
return () => {
|
|
241
|
+
clearListeners.delete(listener);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Preserve log (persist across reload) ────────────────────────────────────
|
|
246
|
+
// Default OFF: the buffer is in-memory only, so a reload starts fresh. When ON,
|
|
247
|
+
// the buffer is saved to storage (debounced) and restored on the next launch.
|
|
248
|
+
|
|
249
|
+
const PRESERVE_KEY = "@react_buoy_console_preserve";
|
|
250
|
+
const BUFFER_KEY = "@react_buoy_console_buffer";
|
|
251
|
+
const SAVE_DEBOUNCE_MS = 600;
|
|
252
|
+
|
|
253
|
+
let preserveLog = false;
|
|
254
|
+
let persistenceReady = false;
|
|
255
|
+
const preserveListeners = new Set<Listener>();
|
|
256
|
+
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
257
|
+
|
|
258
|
+
function emitPreserve(): void {
|
|
259
|
+
for (const listener of preserveListeners) listener();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scheduleSave(): void {
|
|
263
|
+
if (!preserveLog) return;
|
|
264
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
265
|
+
saveTimer = setTimeout(saveBuffer, SAVE_DEBOUNCE_MS);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function saveBuffer(): Promise<void> {
|
|
269
|
+
saveTimer = null;
|
|
270
|
+
if (!preserveLog) return;
|
|
271
|
+
try {
|
|
272
|
+
const serializable = entries.map((entry) => ({
|
|
273
|
+
...entry,
|
|
274
|
+
args: sanitizeArgs(entry.args),
|
|
275
|
+
}));
|
|
276
|
+
await persistentStorage.setItem(BUFFER_KEY, JSON.stringify(serializable));
|
|
277
|
+
} catch {
|
|
278
|
+
// Storage unavailable — preserve is best-effort.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function clearSavedBuffer(): Promise<void> {
|
|
283
|
+
try {
|
|
284
|
+
await persistentStorage.removeItem(BUFFER_KEY);
|
|
285
|
+
} catch {
|
|
286
|
+
// ignore
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Load the persisted preserve-log flag and, if enabled, restore the saved
|
|
292
|
+
* buffer. If disabled, drop any saved buffer so the session starts fresh.
|
|
293
|
+
* Idempotent — safe to call from ConsoleRoot on every mount.
|
|
294
|
+
*/
|
|
295
|
+
export async function initConsolePersistence(): Promise<void> {
|
|
296
|
+
if (persistenceReady) return;
|
|
297
|
+
persistenceReady = true;
|
|
298
|
+
try {
|
|
299
|
+
preserveLog = (await persistentStorage.getItem(PRESERVE_KEY)) === "true";
|
|
300
|
+
} catch {
|
|
301
|
+
preserveLog = false;
|
|
302
|
+
}
|
|
303
|
+
emitPreserve();
|
|
304
|
+
|
|
305
|
+
if (!preserveLog) {
|
|
306
|
+
await clearSavedBuffer();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const raw = await persistentStorage.getItem(BUFFER_KEY);
|
|
311
|
+
if (raw) {
|
|
312
|
+
const saved = JSON.parse(raw) as ConsoleLogEntry[];
|
|
313
|
+
if (Array.isArray(saved) && saved.length) {
|
|
314
|
+
// Prepend restored entries before anything captured this session.
|
|
315
|
+
entries = [...saved, ...entries].slice(-MAX_ENTRIES);
|
|
316
|
+
nextId = Math.max(nextId, ...saved.map((e) => e.id + 1));
|
|
317
|
+
emit();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// Corrupt/absent buffer — ignore.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Whether logs persist across reload. */
|
|
326
|
+
export function getPreserveLog(): boolean {
|
|
327
|
+
return preserveLog;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Toggle preserve-log. Persists the flag; saves or drops the buffer. */
|
|
331
|
+
export function setPreserveLog(next: boolean): void {
|
|
332
|
+
if (preserveLog === next) return;
|
|
333
|
+
preserveLog = next;
|
|
334
|
+
emitPreserve();
|
|
335
|
+
persistentStorage.setItem(PRESERVE_KEY, String(next)).catch(() => {});
|
|
336
|
+
if (next) saveBuffer();
|
|
337
|
+
else clearSavedBuffer();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Subscribe to preserve-log changes. Returns an unsubscribe function. */
|
|
341
|
+
export function subscribePreserveLog(listener: Listener): () => void {
|
|
342
|
+
preserveListeners.add(listener);
|
|
343
|
+
return () => {
|
|
344
|
+
preserveListeners.delete(listener);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sanitizeArgs — deep-convert captured console arguments into JSON-safe values.
|
|
3
|
+
*
|
|
4
|
+
* Captured `args` can hold Errors, functions, bigints, symbols, and circular
|
|
5
|
+
* references. Both the external-sync snapshot (device → desktop) and the
|
|
6
|
+
* preserve-log persistence need plain JSON, so this produces a structural copy
|
|
7
|
+
* (objects/arrays preserved so they stay expandable) with cycles broken.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_DEPTH = 6;
|
|
11
|
+
const MAX_ARRAY = 100;
|
|
12
|
+
const MAX_KEYS = 100;
|
|
13
|
+
|
|
14
|
+
function sanitizeValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {
|
|
15
|
+
if (value === null) return null;
|
|
16
|
+
const t = typeof value;
|
|
17
|
+
if (t === "string" || t === "boolean") return value;
|
|
18
|
+
if (t === "number") return Number.isFinite(value as number) ? value : String(value);
|
|
19
|
+
if (t === "undefined") return "undefined";
|
|
20
|
+
if (t === "bigint") return `${String(value)}n`;
|
|
21
|
+
if (t === "function") return `ƒ ${(value as { name?: string }).name || "(anonymous)"}`;
|
|
22
|
+
if (t === "symbol") return (value as symbol).toString();
|
|
23
|
+
|
|
24
|
+
if (value instanceof Error) {
|
|
25
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (depth >= MAX_DEPTH) return Array.isArray(value) ? "[Array]" : "{…}";
|
|
29
|
+
|
|
30
|
+
const obj = value as object;
|
|
31
|
+
if (seen.has(obj)) return "[Circular]";
|
|
32
|
+
seen.add(obj);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const out = value.slice(0, MAX_ARRAY).map((v) => sanitizeValue(v, depth + 1, seen));
|
|
37
|
+
if (value.length > MAX_ARRAY) out.push(`… ${value.length - MAX_ARRAY} more`);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const out: Record<string, unknown> = {};
|
|
41
|
+
for (const key of Object.keys(obj).slice(0, MAX_KEYS)) {
|
|
42
|
+
try {
|
|
43
|
+
out[key] = sanitizeValue((obj as Record<string, unknown>)[key], depth + 1, seen);
|
|
44
|
+
} catch {
|
|
45
|
+
out[key] = "[Unserializable]";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
} finally {
|
|
50
|
+
seen.delete(obj);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Deep-sanitize an array of console arguments into JSON-safe values. */
|
|
55
|
+
export function sanitizeArgs(args: unknown[]): unknown[] {
|
|
56
|
+
const seen = new WeakSet<object>();
|
|
57
|
+
return args.map((arg) => sanitizeValue(arg, 0, seen));
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consoleSyncAdapter — streams captured console entries from the device to the
|
|
3
|
+
* desktop dashboard via @buoy-gg/external-sync.
|
|
4
|
+
*
|
|
5
|
+
* Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
|
|
6
|
+
* package (so console has no hard dependency on external-sync, matching how the
|
|
7
|
+
* other tools' adapters are written). Auto-discovery in @buoy-gg/core picks it
|
|
8
|
+
* up by the export name and announces the "console" capability.
|
|
9
|
+
*
|
|
10
|
+
* Snapshots MUST be JSON-serializable. Captured `args` can hold Errors,
|
|
11
|
+
* functions, bigints, and circular references, so we deep-sanitize them into
|
|
12
|
+
* plain JSON values (objects/arrays preserved so they stay expandable on the
|
|
13
|
+
* dashboard).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
subscribe,
|
|
18
|
+
getEntries,
|
|
19
|
+
clearEntries,
|
|
20
|
+
type ConsoleLogEntry,
|
|
21
|
+
} from "../store/consoleLogStore";
|
|
22
|
+
import { sanitizeArgs } from "../store/sanitize";
|
|
23
|
+
|
|
24
|
+
interface ConsoleSnapshot {
|
|
25
|
+
entries: ConsoleLogEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const consoleSyncAdapter = {
|
|
29
|
+
version: 1,
|
|
30
|
+
|
|
31
|
+
getSnapshot(): ConsoleSnapshot {
|
|
32
|
+
const entries = getEntries().map((entry) => ({
|
|
33
|
+
...entry,
|
|
34
|
+
args: sanitizeArgs(entry.args),
|
|
35
|
+
}));
|
|
36
|
+
return { entries };
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
subscribe(onChange: () => void): () => void {
|
|
40
|
+
return subscribe(onChange);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
actions: {
|
|
44
|
+
clearEntries(): { cleared: true } {
|
|
45
|
+
clearEntries();
|
|
46
|
+
return { cleared: true };
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|