@zerotal/devtools 1.6.3 → 1.7.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/CHANGELOG.md +329 -0
- package/api-surface.md +298 -0
- package/package.json +5 -4
- package/src/DevtoolsInjectionMiddleware.ts +41 -4
- package/src/RequestTrace.ts +106 -1
- package/src/TraceStore.ts +12 -0
- package/src/activity.ts +116 -0
- package/src/callsite.ts +146 -0
- package/src/client/filter.ts +108 -0
- package/src/client/index.ts +127 -0
- package/src/client/metrics.ts +98 -0
- package/src/client/registry.ts +87 -0
- package/src/client/state.ts +350 -0
- package/src/client/tabs/all.ts +323 -0
- package/src/client/tabs/app.ts +293 -0
- package/src/client/tabs/cache.ts +50 -0
- package/src/client/tabs/channel.ts +264 -0
- package/src/client/tabs/exceptions.ts +69 -0
- package/src/client/tabs/jobs.ts +51 -0
- package/src/client/tabs/live.ts +66 -0
- package/src/client/tabs/logs.ts +45 -0
- package/src/client/tabs/mail.ts +60 -0
- package/src/client/tabs/queries.ts +125 -0
- package/src/client/tabs/request.ts +75 -0
- package/src/client/tabs/sections.ts +115 -0
- package/src/client/tabs/timeline.ts +133 -0
- package/src/client/tabs/types.ts +68 -0
- package/src/client/transport.ts +81 -0
- package/src/client/tree.ts +138 -0
- package/src/client/ui/format.ts +137 -0
- package/src/client/ui/render.ts +87 -0
- package/src/client/ui/shell.ts +560 -0
- package/src/client/ui/theme.ts +445 -0
- package/src/client-auto.ts +1 -1
- package/src/config.ts +77 -2
- package/src/dashboard-auto.ts +1 -1
- package/src/editor.ts +107 -0
- package/src/enabled.ts +59 -0
- package/src/index.ts +19 -3
- package/src/map.ts +213 -0
- package/src/provider/DevtoolsProvider.ts +32 -7
- package/src/redaction.ts +161 -20
- package/src/tracing.ts +261 -29
- package/src/client.ts +0 -1048
- package/src/panel-app.js +0 -519
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small formatters every tab shares.
|
|
3
|
+
*
|
|
4
|
+
* They were nine closures inside `DevTools.start()`, which meant a tab could not
|
|
5
|
+
* be moved into a file of its own without taking copies of them with it. Nothing
|
|
6
|
+
* here touches the DOM or the store — it is all value-in, string-out.
|
|
7
|
+
*/
|
|
8
|
+
import { editorUrl, shortLocation, type SourceLocation } from "../../editor.ts";
|
|
9
|
+
import type { EditorSettings } from "../state.ts";
|
|
10
|
+
|
|
11
|
+
const ESCAPES: Record<string, string> = {
|
|
12
|
+
"&": "&",
|
|
13
|
+
"<": "<",
|
|
14
|
+
">": ">",
|
|
15
|
+
'"': """,
|
|
16
|
+
"'": "'",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* HTML-escape a value for interpolation into markup **or** an attribute.
|
|
21
|
+
*
|
|
22
|
+
* Quotes are escaped as well as angle brackets, which is what makes it safe for
|
|
23
|
+
* `title="…"` and for the mail preview's `srcdoc="…"` — the panel renders text it
|
|
24
|
+
* did not write, on the app's own origin.
|
|
25
|
+
*/
|
|
26
|
+
export function esc(s: unknown): string {
|
|
27
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ESCAPES[c]!);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Drop the trailing zeros a fixed-precision number leaves behind: `3.0` → `3`. */
|
|
31
|
+
function _trim(text: string): string {
|
|
32
|
+
return text.includes(".") ? text.replace(/0+$/, "").replace(/\.$/, "") : text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A duration, at a precision worth reading.
|
|
37
|
+
*
|
|
38
|
+
* Precision scales with magnitude, because the interesting digits move: at 400ms
|
|
39
|
+
* nobody cares about the decimal, and at 0.4ms the decimal is the whole number.
|
|
40
|
+
* Both ends were wrong before. This interpolated the value raw, so anything
|
|
41
|
+
* measured with `performance.now()` printed its full float — the status bar read
|
|
42
|
+
* `3.6370999999926426ms` — while anything a caller had already rounded printed
|
|
43
|
+
* `0ms` for a query that plainly took time.
|
|
44
|
+
*/
|
|
45
|
+
export function fmt(ms: number): string {
|
|
46
|
+
if (!Number.isFinite(ms) || ms < 0) return "—";
|
|
47
|
+
if (ms === 0) return "0ms";
|
|
48
|
+
if (ms >= 1000) return `${_trim((ms / 1000).toFixed(2))}s`;
|
|
49
|
+
if (ms >= 100) return `${Math.round(ms)}ms`;
|
|
50
|
+
if (ms >= 1) return `${_trim(ms.toFixed(1))}ms`;
|
|
51
|
+
return `${_trim(ms.toFixed(2))}ms`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A byte count as KB or MB. */
|
|
55
|
+
export function fmtMem(b: number): string {
|
|
56
|
+
return b >= 1_048_576 ? `${(b / 1_048_576).toFixed(1)} MB` : `${(b / 1024).toFixed(0)} KB`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The colour class for a request duration — empty for "unremarkable". */
|
|
60
|
+
export function dCls(ms: number): string {
|
|
61
|
+
return ms > 1000 ? "red" : ms > 300 ? "yellow" : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The colour class for a status code. */
|
|
65
|
+
export function scCls(s: number): string {
|
|
66
|
+
return s >= 500 ? "srv" : s >= 400 ? "cli" : s >= 300 ? "redir" : "ok";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One channel value, as a single line of text. */
|
|
70
|
+
export function fmtCell(v: unknown): string {
|
|
71
|
+
if (v === null || v === undefined) return "";
|
|
72
|
+
if (typeof v === "object") return JSON.stringify(v) ?? "";
|
|
73
|
+
return String(v);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A badge chip, accented by hashing its own text.
|
|
78
|
+
*
|
|
79
|
+
* `partial` and `deferred` need to be tellable apart at a glance, and the only
|
|
80
|
+
* way to do that without devtools holding a list of every value every package
|
|
81
|
+
* might use is to derive the colour from the value itself. Stable per value, so
|
|
82
|
+
* a badge keeps its colour between requests.
|
|
83
|
+
*/
|
|
84
|
+
export function chipFor(text: string, warn: boolean): string {
|
|
85
|
+
if (warn) return `<span class="chip warn">${esc(text)}</span>`;
|
|
86
|
+
let hash = 0;
|
|
87
|
+
for (let i = 0; i < text.length; i++) hash = (hash * 31 + text.charCodeAt(i)) >>> 0;
|
|
88
|
+
return `<span class="chip a${hash % 6}">${esc(text)}</span>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A copy-to-clipboard button carrying its own payload.
|
|
93
|
+
*
|
|
94
|
+
* The text rides in a data attribute rather than being read back out of the
|
|
95
|
+
* rendered DOM, because what you want on the clipboard is rarely what is on
|
|
96
|
+
* screen — the SQL without its duration bar, the log line without its offset.
|
|
97
|
+
* The shell's delegated handler does the copying; this only marks the target.
|
|
98
|
+
*/
|
|
99
|
+
export function copyBtn(text: string, label = "Copy"): string {
|
|
100
|
+
if (!text) return "";
|
|
101
|
+
return `<button class="cpy" data-copy="${esc(text)}" title="${esc(label)}">⧉</button>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The table an N+1 warning's SQL reads from, for the suppression call. */
|
|
105
|
+
export function tableFrom(sql: string): string {
|
|
106
|
+
return /from\s+[`'"[]?(\w+)/i.exec(sql)?.[1] ?? "table_name";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A source location, as a link to the editor when there is one to make.
|
|
111
|
+
*
|
|
112
|
+
* The panel renders a location the same way everywhere it appears — a query's
|
|
113
|
+
* call site, a log line's, a stack frame, a prop's render source — because the
|
|
114
|
+
* gesture is the same one every time: this is where it happened, go there.
|
|
115
|
+
*
|
|
116
|
+
* Falls back to plain text when `editor` is null or the location has no file. A
|
|
117
|
+
* location worth showing is still worth showing when it cannot be opened.
|
|
118
|
+
*
|
|
119
|
+
* @param location - Where, or null/undefined for nothing at all.
|
|
120
|
+
* @param editor - The panel's resolved editor settings.
|
|
121
|
+
* @param extraClass - Appended to the element's class, for per-surface spacing.
|
|
122
|
+
*/
|
|
123
|
+
export function sourceLink(
|
|
124
|
+
location: SourceLocation | null | undefined,
|
|
125
|
+
editor: EditorSettings,
|
|
126
|
+
extraClass = "",
|
|
127
|
+
): string {
|
|
128
|
+
if (!location?.file) return "";
|
|
129
|
+
const label = shortLocation(location);
|
|
130
|
+
const title = location.function
|
|
131
|
+
? `${location.function} — ${location.file}:${location.line}`
|
|
132
|
+
: `${location.file}:${location.line}`;
|
|
133
|
+
const url = editorUrl(location, editor.editor, editor.editorPathMap);
|
|
134
|
+
const cls = `src${extraClass ? ` ${extraClass}` : ""}`;
|
|
135
|
+
if (!url) return `<span class="${cls}" title="${esc(title)}">${esc(label)}</span>`;
|
|
136
|
+
return `<a class="${cls} link" href="${esc(url)}" title="${esc(title)}">${esc(label)}</a>`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A keyed list reconciler, in about sixty lines and with no dependency.
|
|
3
|
+
*
|
|
4
|
+
* The panel used to redraw `#content` wholesale on every trace, which on the All
|
|
5
|
+
* tab — the one that redraws most, because every request changes it — threw away
|
|
6
|
+
* the scroll position, every open `<details>`, and the caret in the filter box,
|
|
7
|
+
* fifty times a second under load. Keying the rows means an arriving request
|
|
8
|
+
* inserts one node and touches nothing else.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately not a virtual DOM. The panel is bundled on demand and injected
|
|
11
|
+
* into arbitrary apps, so it stays dependency-free and small; a list diff is the
|
|
12
|
+
* only reconciliation any of this needs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Where a reconciler stores each child's identity. */
|
|
16
|
+
const KEY_ATTR = "data-k";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Make `host`'s element children match `items`, in order, reusing by key.
|
|
20
|
+
*
|
|
21
|
+
* A child whose key survives is moved rather than rebuilt, so anything the
|
|
22
|
+
* browser owns inside it — scroll offset, `<details>` state, an iframe's loaded
|
|
23
|
+
* document, text selection — survives with it.
|
|
24
|
+
*
|
|
25
|
+
* @param host - The element whose children are the list. Must hold nothing else.
|
|
26
|
+
* @param items - The list, in the order it should appear.
|
|
27
|
+
* @param keyOf - A stable identity per item. Two items must never share one.
|
|
28
|
+
* @param create - Build the element for an item seen for the first time.
|
|
29
|
+
* @param update - Bring a reused element up to date. Called for every survivor.
|
|
30
|
+
*/
|
|
31
|
+
export function reconcile<T>(
|
|
32
|
+
host: HTMLElement,
|
|
33
|
+
items: readonly T[],
|
|
34
|
+
keyOf: (item: T) => string,
|
|
35
|
+
create: (item: T) => HTMLElement,
|
|
36
|
+
update?: (el: HTMLElement, item: T) => void,
|
|
37
|
+
): void {
|
|
38
|
+
const existing = new Map<string, HTMLElement>();
|
|
39
|
+
// Anything without a key was not put there by a previous reconcile — an empty
|
|
40
|
+
// state, a message the host wrote directly — so it is not part of the list and
|
|
41
|
+
// has to go, or it lingers above the rows that replaced it.
|
|
42
|
+
const strays: Element[] = [];
|
|
43
|
+
for (const child of Array.from(host.children)) {
|
|
44
|
+
const key = child.getAttribute(KEY_ATTR);
|
|
45
|
+
if (key === null) strays.push(child);
|
|
46
|
+
else existing.set(key, child as HTMLElement);
|
|
47
|
+
}
|
|
48
|
+
for (const stray of strays) stray.remove();
|
|
49
|
+
|
|
50
|
+
// Walks forward through the host's children alongside `items`. Anything
|
|
51
|
+
// already in the right place is left untouched, which is what keeps a steady
|
|
52
|
+
// list from generating DOM writes at all.
|
|
53
|
+
let cursor: ChildNode | null = host.firstChild;
|
|
54
|
+
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
const key = keyOf(item);
|
|
57
|
+
let el = existing.get(key);
|
|
58
|
+
if (el) {
|
|
59
|
+
existing.delete(key);
|
|
60
|
+
update?.(el, item);
|
|
61
|
+
} else {
|
|
62
|
+
el = create(item);
|
|
63
|
+
el.setAttribute(KEY_ATTR, key);
|
|
64
|
+
}
|
|
65
|
+
if (cursor === el) {
|
|
66
|
+
cursor = el.nextSibling;
|
|
67
|
+
} else {
|
|
68
|
+
host.insertBefore(el, cursor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Whatever the walk did not claim is gone from the list.
|
|
73
|
+
for (const el of existing.values()) el.remove();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build an element from a markup string.
|
|
78
|
+
*
|
|
79
|
+
* The tabs are string builders — the shape they render is fixed and a template
|
|
80
|
+
* literal reads far better than twenty `createElement` calls — so the reconciler
|
|
81
|
+
* needs a way back from markup to a node it can key and move.
|
|
82
|
+
*/
|
|
83
|
+
export function el(html: string): HTMLElement {
|
|
84
|
+
const template = document.createElement("template");
|
|
85
|
+
template.innerHTML = html.trim();
|
|
86
|
+
return template.content.firstElementChild as HTMLElement;
|
|
87
|
+
}
|