@reckona/mreact-devtools 0.0.66 → 0.0.67
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/package.json +3 -2
- package/src/index.ts +73 -0
- package/src/overlay.ts +246 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reckona/mreact-devtools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.67",
|
|
4
4
|
"description": "Runtime event bridge for mreact developer tooling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"debugging",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"dist/**/*.js",
|
|
25
25
|
"dist/**/*.js.map",
|
|
26
26
|
"dist/**/*.d.ts",
|
|
27
|
-
"dist/**/*.d.ts.map"
|
|
27
|
+
"dist/**/*.d.ts.map",
|
|
28
|
+
"src/**/*"
|
|
28
29
|
],
|
|
29
30
|
"type": "module",
|
|
30
31
|
"sideEffects": false,
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface DevtoolsEvent {
|
|
2
|
+
package: string;
|
|
3
|
+
timestamp?: number;
|
|
4
|
+
type: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type DevtoolsListener = (event: DevtoolsEvent) => void;
|
|
9
|
+
|
|
10
|
+
export interface Devtools {
|
|
11
|
+
dispose(): void;
|
|
12
|
+
emit(event: DevtoolsEvent): void;
|
|
13
|
+
events(): DevtoolsEvent[];
|
|
14
|
+
subscribe(listener: DevtoolsListener): () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
// eslint-disable-next-line no-var
|
|
19
|
+
var __mreactDevtools: Devtools | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDevtools(): Devtools {
|
|
23
|
+
const recorded: DevtoolsEvent[] = [];
|
|
24
|
+
const listeners = new Set<DevtoolsListener>();
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
dispose() {
|
|
28
|
+
listeners.clear();
|
|
29
|
+
recorded.length = 0;
|
|
30
|
+
if (globalThis.__mreactDevtools === this) {
|
|
31
|
+
globalThis.__mreactDevtools = undefined;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
emit(event) {
|
|
35
|
+
recorded.push(event);
|
|
36
|
+
|
|
37
|
+
for (const listener of Array.from(listeners)) {
|
|
38
|
+
listener(event);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
events() {
|
|
42
|
+
return [...recorded];
|
|
43
|
+
},
|
|
44
|
+
subscribe(listener) {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
listeners.delete(listener);
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function installDevtools(devtools: Devtools = createDevtools()): Devtools {
|
|
55
|
+
globalThis.__mreactDevtools = devtools;
|
|
56
|
+
|
|
57
|
+
return devtools;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getInstalledDevtools(): Devtools | undefined {
|
|
61
|
+
return globalThis.__mreactDevtools;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function emitMreactDevtoolsEvent(
|
|
65
|
+
packageName: string,
|
|
66
|
+
event: { type: string } & Record<string, unknown>,
|
|
67
|
+
): void {
|
|
68
|
+
globalThis.__mreactDevtools?.emit?.({
|
|
69
|
+
package: packageName,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
...event,
|
|
72
|
+
});
|
|
73
|
+
}
|
package/src/overlay.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDevtools,
|
|
3
|
+
getInstalledDevtools,
|
|
4
|
+
installDevtools,
|
|
5
|
+
type Devtools,
|
|
6
|
+
type DevtoolsEvent,
|
|
7
|
+
} from "./index.js";
|
|
8
|
+
|
|
9
|
+
export type { Devtools, DevtoolsEvent, DevtoolsListener } from "./index.js";
|
|
10
|
+
|
|
11
|
+
export type DevtoolsOverlayTab = "query" | "reactive" | "router";
|
|
12
|
+
|
|
13
|
+
export interface DevtoolsOverlayOptions {
|
|
14
|
+
devtools?: Devtools | undefined;
|
|
15
|
+
document?: Document | undefined;
|
|
16
|
+
maxEvents?: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MountedDevtoolsOverlay {
|
|
20
|
+
devtools: Devtools;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
element: HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TabDefinition {
|
|
26
|
+
label: string;
|
|
27
|
+
tab: DevtoolsOverlayTab;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tabs: readonly TabDefinition[] = [
|
|
31
|
+
{ label: "Reactive", tab: "reactive" },
|
|
32
|
+
{ label: "Query", tab: "query" },
|
|
33
|
+
{ label: "Router", tab: "router" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const defaultMaxEvents = 200;
|
|
37
|
+
|
|
38
|
+
export function mountDevtoolsOverlay(
|
|
39
|
+
options: DevtoolsOverlayOptions = {},
|
|
40
|
+
): MountedDevtoolsOverlay {
|
|
41
|
+
const ownerDocument = options.document ?? documentFromGlobal();
|
|
42
|
+
const maxEvents = normalizeMaxEvents(options.maxEvents);
|
|
43
|
+
const devtools = options.devtools ?? getInstalledDevtools() ?? installDevtools(createDevtools());
|
|
44
|
+
const events = devtools.events().slice(-maxEvents);
|
|
45
|
+
const element = ownerDocument.createElement("section");
|
|
46
|
+
const tabList = ownerDocument.createElement("nav");
|
|
47
|
+
const eventList = ownerDocument.createElement("ol");
|
|
48
|
+
let activeTab: DevtoolsOverlayTab = "reactive";
|
|
49
|
+
let disposed = false;
|
|
50
|
+
let renderScheduled = false;
|
|
51
|
+
|
|
52
|
+
element.dataset.mreactDevtoolsOverlay = "";
|
|
53
|
+
element.setAttribute("aria-label", "mreact devtools");
|
|
54
|
+
Object.assign(element.style, {
|
|
55
|
+
background: "#10151f",
|
|
56
|
+
border: "1px solid #3b4350",
|
|
57
|
+
borderRadius: "8px",
|
|
58
|
+
bottom: "16px",
|
|
59
|
+
boxShadow: "0 18px 50px rgb(0 0 0 / 0.35)",
|
|
60
|
+
color: "#eef2f8",
|
|
61
|
+
display: "grid",
|
|
62
|
+
font: "12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
63
|
+
gap: "8px",
|
|
64
|
+
maxHeight: "min(420px, calc(100vh - 32px))",
|
|
65
|
+
overflow: "hidden",
|
|
66
|
+
padding: "10px",
|
|
67
|
+
position: "fixed",
|
|
68
|
+
right: "16px",
|
|
69
|
+
width: "min(520px, calc(100vw - 32px))",
|
|
70
|
+
zIndex: "2147483647",
|
|
71
|
+
});
|
|
72
|
+
Object.assign(tabList.style, {
|
|
73
|
+
display: "grid",
|
|
74
|
+
gap: "6px",
|
|
75
|
+
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
|
76
|
+
});
|
|
77
|
+
Object.assign(eventList.style, {
|
|
78
|
+
display: "grid",
|
|
79
|
+
gap: "6px",
|
|
80
|
+
listStyle: "none",
|
|
81
|
+
margin: "0",
|
|
82
|
+
overflow: "auto",
|
|
83
|
+
padding: "0",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
element.append(tabList, eventList);
|
|
87
|
+
ownerDocument.body.append(element);
|
|
88
|
+
|
|
89
|
+
const unsubscribe = devtools.subscribe((event) => {
|
|
90
|
+
events.push(event);
|
|
91
|
+
if (events.length > maxEvents) {
|
|
92
|
+
events.splice(0, events.length - maxEvents);
|
|
93
|
+
}
|
|
94
|
+
scheduleRender();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
render();
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
devtools,
|
|
101
|
+
dispose() {
|
|
102
|
+
disposed = true;
|
|
103
|
+
unsubscribe();
|
|
104
|
+
element.remove();
|
|
105
|
+
},
|
|
106
|
+
element,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function render(): void {
|
|
110
|
+
if (disposed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tabList.replaceChildren(...tabs.map((tab) => renderTab(ownerDocument, tab)));
|
|
115
|
+
const visibleEvents = [...events].filter((event) => eventTab(event) === activeTab).reverse();
|
|
116
|
+
|
|
117
|
+
if (visibleEvents.length === 0) {
|
|
118
|
+
const empty = ownerDocument.createElement("li");
|
|
119
|
+
empty.textContent = "No events";
|
|
120
|
+
Object.assign(empty.style, {
|
|
121
|
+
color: "#9aa8bc",
|
|
122
|
+
padding: "12px 4px",
|
|
123
|
+
});
|
|
124
|
+
eventList.replaceChildren(empty);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
eventList.replaceChildren(...visibleEvents.map((event) => renderEvent(ownerDocument, event)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scheduleRender(): void {
|
|
132
|
+
if (renderScheduled || disposed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
renderScheduled = true;
|
|
137
|
+
queueMicrotask(() => {
|
|
138
|
+
renderScheduled = false;
|
|
139
|
+
render();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderTab(doc: Document, definition: TabDefinition): HTMLButtonElement {
|
|
144
|
+
const button = doc.createElement("button");
|
|
145
|
+
const count = events.filter((event) => eventTab(event) === definition.tab).length;
|
|
146
|
+
button.type = "button";
|
|
147
|
+
button.textContent = `${definition.label} ${count}`;
|
|
148
|
+
button.setAttribute("aria-pressed", String(definition.tab === activeTab));
|
|
149
|
+
Object.assign(button.style, {
|
|
150
|
+
background: definition.tab === activeTab ? "#d8e7ff" : "#1b2330",
|
|
151
|
+
border: "1px solid #3b4350",
|
|
152
|
+
borderRadius: "6px",
|
|
153
|
+
color: definition.tab === activeTab ? "#111827" : "#d8e7ff",
|
|
154
|
+
cursor: "pointer",
|
|
155
|
+
font: "inherit",
|
|
156
|
+
minHeight: "32px",
|
|
157
|
+
overflow: "hidden",
|
|
158
|
+
padding: "6px 8px",
|
|
159
|
+
textOverflow: "ellipsis",
|
|
160
|
+
whiteSpace: "nowrap",
|
|
161
|
+
});
|
|
162
|
+
button.addEventListener("click", () => {
|
|
163
|
+
activeTab = definition.tab;
|
|
164
|
+
render();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return button;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderEvent(doc: Document, event: DevtoolsEvent): HTMLLIElement {
|
|
172
|
+
const item = doc.createElement("li");
|
|
173
|
+
const title = doc.createElement("div");
|
|
174
|
+
const meta = doc.createElement("div");
|
|
175
|
+
const details = doc.createElement("pre");
|
|
176
|
+
|
|
177
|
+
title.textContent = event.type;
|
|
178
|
+
meta.textContent = [event.package, formatTimestamp(event.timestamp)].filter(Boolean).join(" | ");
|
|
179
|
+
details.textContent = eventDetails(event);
|
|
180
|
+
|
|
181
|
+
Object.assign(item.style, {
|
|
182
|
+
background: "#161d28",
|
|
183
|
+
border: "1px solid #303846",
|
|
184
|
+
borderRadius: "6px",
|
|
185
|
+
display: "grid",
|
|
186
|
+
gap: "3px",
|
|
187
|
+
padding: "8px",
|
|
188
|
+
});
|
|
189
|
+
Object.assign(title.style, {
|
|
190
|
+
color: "#ffffff",
|
|
191
|
+
fontWeight: "700",
|
|
192
|
+
overflowWrap: "anywhere",
|
|
193
|
+
});
|
|
194
|
+
Object.assign(meta.style, {
|
|
195
|
+
color: "#9aa8bc",
|
|
196
|
+
overflowWrap: "anywhere",
|
|
197
|
+
});
|
|
198
|
+
Object.assign(details.style, {
|
|
199
|
+
color: "#c8d7ec",
|
|
200
|
+
margin: "0",
|
|
201
|
+
overflow: "auto",
|
|
202
|
+
whiteSpace: "pre-wrap",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
item.append(title, meta, details);
|
|
206
|
+
|
|
207
|
+
return item;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function eventDetails(event: DevtoolsEvent): string {
|
|
211
|
+
const { package: _packageName, timestamp: _timestamp, type: _type, ...details } = event;
|
|
212
|
+
|
|
213
|
+
return Object.keys(details).length === 0 ? "{}" : JSON.stringify(details);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function eventTab(event: DevtoolsEvent): DevtoolsOverlayTab {
|
|
217
|
+
const search = `${event.package} ${event.type}`.toLowerCase();
|
|
218
|
+
|
|
219
|
+
if (search.includes("router") || search.includes("route") || search.includes("navigation")) {
|
|
220
|
+
return "router";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (search.includes("query") || search.includes("mutation")) {
|
|
224
|
+
return "query";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return "reactive";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatTimestamp(timestamp: number | undefined): string {
|
|
231
|
+
return typeof timestamp === "number" ? new Date(timestamp).toISOString() : "";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeMaxEvents(value: number | undefined): number {
|
|
235
|
+
return Number.isFinite(value) && value !== undefined && value > 0
|
|
236
|
+
? Math.floor(value)
|
|
237
|
+
: defaultMaxEvents;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function documentFromGlobal(): Document {
|
|
241
|
+
if (typeof document === "undefined") {
|
|
242
|
+
throw new Error("mountDevtoolsOverlay requires a browser document.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return document;
|
|
246
|
+
}
|