@dimina-kit/inspect 0.3.0 → 0.4.0-dev.20260716163758
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/README.md +37 -13
- package/dist/appdata-accumulator.js +218 -0
- package/dist/appdata-panel-view.js +64 -0
- package/dist/appdata-source.js +1 -0
- package/dist/compile-panel-view.js +92 -0
- package/dist/compile-source.js +1 -0
- package/dist/compile-types.js +4 -0
- package/dist/connected-appdata-panel.js +32 -0
- package/dist/connected-compile-panel.js +72 -0
- package/dist/index.js +1 -0
- package/dist/panel.js +5 -0
- package/dist/use-active-bridge-id.js +59 -0
- package/dist/wxml-extract.js +39 -103
- package/package.json +4 -1
- package/src/appdata-accumulator.test.ts +83 -0
- package/src/appdata-accumulator.ts +246 -0
- package/src/appdata-panel-view-empty-state.test.tsx +65 -0
- package/src/appdata-panel-view.test.tsx +49 -0
- package/src/appdata-panel-view.tsx +165 -0
- package/src/appdata-source.ts +18 -0
- package/src/compile-panel-view-logs.test.tsx +269 -0
- package/src/compile-panel-view.test.tsx +194 -0
- package/src/compile-panel-view.tsx +236 -0
- package/src/compile-source.ts +35 -0
- package/src/compile-types.ts +35 -0
- package/src/connected-appdata-panel.test.tsx +426 -0
- package/src/connected-appdata-panel.tsx +62 -0
- package/src/connected-compile-panel.test.tsx +500 -0
- package/src/connected-compile-panel.tsx +91 -0
- package/src/index.ts +16 -0
- package/src/panel.tsx +5 -0
- package/src/use-active-bridge-id.test.tsx +202 -0
- package/src/use-active-bridge-id.ts +71 -0
- package/src/wxml-extract.test.ts +70 -0
- package/src/wxml-extract.ts +38 -100
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The pure 编译 panel view: the host's compile-status events and per-line
|
|
2
|
+
// compiler log merged — in the VIEW layer only — into one oldest-first
|
|
3
|
+
// (chronological) timeline with a text filter. Pure presentation — feed
|
|
4
|
+
// state lives in the connected container (or the host, when it renders this
|
|
5
|
+
// view directly).
|
|
6
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
7
|
+
import type { CompileEvent, CompileLogEntry } from './compile-types.js'
|
|
8
|
+
|
|
9
|
+
/** Join the truthy class fragments (the panel's only class-composition need). */
|
|
10
|
+
function cn(...parts: Array<string | false | undefined>): string {
|
|
11
|
+
return parts.filter(Boolean).join(' ')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CompilePanelProps {
|
|
15
|
+
/** Chronological (oldest-first) compile-event log. */
|
|
16
|
+
events: CompileEvent[]
|
|
17
|
+
/**
|
|
18
|
+
* Chronological (oldest-first) per-line compiler log. Optional: omitting it
|
|
19
|
+
* keeps the event-only behaviour (incl. the 暂无编译 empty state).
|
|
20
|
+
*/
|
|
21
|
+
logs?: CompileLogEntry[]
|
|
22
|
+
/** Clear the log (the owner empties both stores). */
|
|
23
|
+
onClear: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatTime(at: number): string {
|
|
27
|
+
const d = new Date(at)
|
|
28
|
+
return [d.getHours(), d.getMinutes(), d.getSeconds()]
|
|
29
|
+
.map((n) => String(n).padStart(2, '0'))
|
|
30
|
+
.join(':')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Status → row/badge accent. Unknown statuses fall back to muted text. */
|
|
34
|
+
function statusClass(status: string): string {
|
|
35
|
+
if (status === 'error') return 'text-red-500'
|
|
36
|
+
if (status === 'compiling') return 'text-amber-500'
|
|
37
|
+
if (status === 'ready') return 'text-emerald-600'
|
|
38
|
+
return 'text-text-muted'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Extract the display text from a timeline item for filter matching. */
|
|
42
|
+
function itemText(item: TimelineItem): string {
|
|
43
|
+
return item.kind === 'event' ? item.event.message : item.log.text
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface EventItem {
|
|
47
|
+
kind: 'event'
|
|
48
|
+
at: number
|
|
49
|
+
/** Arrival counter shared with logs — the same-`at` tie-break carrier. */
|
|
50
|
+
seq?: number
|
|
51
|
+
event: CompileEvent
|
|
52
|
+
/** Elapsed ms when this ready event pairs with the preceding compiling. */
|
|
53
|
+
durationMs: number | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface LogItem {
|
|
57
|
+
kind: 'log'
|
|
58
|
+
at: number
|
|
59
|
+
/** Arrival counter shared with events — the same-`at` tie-break carrier. */
|
|
60
|
+
seq?: number
|
|
61
|
+
log: CompileLogEntry
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type TimelineItem = EventItem | LogItem
|
|
65
|
+
|
|
66
|
+
export function CompilePanel({ events, logs = [], onClear }: CompilePanelProps) {
|
|
67
|
+
const latest = events.length > 0 ? events[events.length - 1]! : null
|
|
68
|
+
const [filter, setFilter] = useState('')
|
|
69
|
+
|
|
70
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
71
|
+
const bottomRef = useRef<HTMLDivElement>(null)
|
|
72
|
+
const shouldAutoScroll = useRef(true)
|
|
73
|
+
|
|
74
|
+
// Track whether the user has scrolled away from the bottom.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const container = scrollRef.current
|
|
77
|
+
if (!container) return
|
|
78
|
+
const handleScroll = () => {
|
|
79
|
+
const { scrollTop, scrollHeight, clientHeight } = container
|
|
80
|
+
shouldAutoScroll.current = scrollHeight - scrollTop - clientHeight < 20
|
|
81
|
+
}
|
|
82
|
+
container.addEventListener('scroll', handleScroll)
|
|
83
|
+
return () => container.removeEventListener('scroll', handleScroll)
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
const totalItems = events.length + logs.length
|
|
87
|
+
|
|
88
|
+
// Auto-scroll to bottom when new items arrive. Optional-call: jsdom-class
|
|
89
|
+
// environments don't implement scrollIntoView.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (shouldAutoScroll.current) {
|
|
92
|
+
bottomRef.current?.scrollIntoView?.({ behavior: 'auto' })
|
|
93
|
+
}
|
|
94
|
+
}, [totalItems])
|
|
95
|
+
|
|
96
|
+
const timeline = useMemo<TimelineItem[]>(() => {
|
|
97
|
+
const items: TimelineItem[] = []
|
|
98
|
+
events.forEach((event, index) => {
|
|
99
|
+
// Duration pairs strictly with the IMMEDIATELY preceding event: a
|
|
100
|
+
// ready right after a compiling shows the elapsed compile time; any
|
|
101
|
+
// intervening event (e.g. an error) breaks the pair.
|
|
102
|
+
const previous = index > 0 ? events[index - 1]! : null
|
|
103
|
+
const durationMs =
|
|
104
|
+
event.status === 'ready' && previous?.status === 'compiling'
|
|
105
|
+
? event.at - previous.at
|
|
106
|
+
: null
|
|
107
|
+
items.push({ kind: 'event', at: event.at, seq: event.seq, event, durationMs })
|
|
108
|
+
})
|
|
109
|
+
for (const log of logs) {
|
|
110
|
+
items.push({ kind: 'log', at: log.at, seq: log.seq, log })
|
|
111
|
+
}
|
|
112
|
+
// Oldest first (chronological). Within a same-`at` tie (millisecond
|
|
113
|
+
// stamps — an event and the logs of the same compile collide routinely)
|
|
114
|
+
// the shared monotonic `seq` keeps ARRIVAL order.
|
|
115
|
+
return items.sort((a, b) => {
|
|
116
|
+
if (a.at !== b.at) return a.at - b.at
|
|
117
|
+
if (a.seq !== undefined && b.seq !== undefined) return a.seq - b.seq
|
|
118
|
+
return 0
|
|
119
|
+
})
|
|
120
|
+
}, [events, logs])
|
|
121
|
+
|
|
122
|
+
const filtered = useMemo(() => {
|
|
123
|
+
if (!filter) return timeline
|
|
124
|
+
const lower = filter.toLowerCase()
|
|
125
|
+
return timeline.filter((item) => itemText(item).toLowerCase().includes(lower))
|
|
126
|
+
}, [timeline, filter])
|
|
127
|
+
|
|
128
|
+
const isEmpty = events.length === 0 && logs.length === 0
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex flex-col h-full w-full min-h-0 text-[12px]">
|
|
132
|
+
{/* Header: current-status badge + filter + 清空. */}
|
|
133
|
+
<div className="flex items-center gap-2 px-2 h-8 shrink-0 border-b border-border-subtle">
|
|
134
|
+
{latest ? (
|
|
135
|
+
<span
|
|
136
|
+
data-compile-current
|
|
137
|
+
data-status={latest.status}
|
|
138
|
+
className={cn('truncate font-medium shrink-0', statusClass(latest.status))}
|
|
139
|
+
>
|
|
140
|
+
{latest.message}
|
|
141
|
+
</span>
|
|
142
|
+
) : (
|
|
143
|
+
<span className="text-text-muted shrink-0">编译信息</span>
|
|
144
|
+
)}
|
|
145
|
+
<div className="flex-1 min-w-0" />
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
value={filter}
|
|
149
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
150
|
+
placeholder="过滤..."
|
|
151
|
+
className="h-6 px-2 text-[12px] rounded-sm border border-border-subtle bg-surface
|
|
152
|
+
text-text placeholder:text-text-dim
|
|
153
|
+
focus:outline-none focus:border-text-muted
|
|
154
|
+
w-40 shrink-0"
|
|
155
|
+
/>
|
|
156
|
+
{filter && (
|
|
157
|
+
<span className="text-[11px] text-text-muted tabular-nums shrink-0">
|
|
158
|
+
{filtered.length}/{timeline.length}
|
|
159
|
+
</span>
|
|
160
|
+
)}
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={onClear}
|
|
164
|
+
className="px-2 h-6 rounded-sm text-text-muted hover:text-text hover:bg-surface-2 transition-colors shrink-0"
|
|
165
|
+
>
|
|
166
|
+
清空
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div ref={scrollRef} className="flex-1 min-h-0 overflow-auto">
|
|
171
|
+
{isEmpty ? (
|
|
172
|
+
<div className="flex items-center justify-center h-full text-text-muted">
|
|
173
|
+
暂无编译信息
|
|
174
|
+
</div>
|
|
175
|
+
) : filtered.length === 0 ? (
|
|
176
|
+
<div className="flex items-center justify-center h-full text-text-muted">
|
|
177
|
+
无匹配日志
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
<ul className="px-2 py-1 space-y-0.5">
|
|
181
|
+
{filtered.map((item, index) =>
|
|
182
|
+
item.kind === 'event' ? (
|
|
183
|
+
<li
|
|
184
|
+
key={`e-${item.at}-${index}`}
|
|
185
|
+
data-compile-row
|
|
186
|
+
data-status={item.event.status}
|
|
187
|
+
className="flex items-baseline gap-2 leading-5"
|
|
188
|
+
>
|
|
189
|
+
<span className="text-text-muted tabular-nums shrink-0">
|
|
190
|
+
{formatTime(item.at)}
|
|
191
|
+
</span>
|
|
192
|
+
<span className={cn('break-all', statusClass(item.event.status))}>
|
|
193
|
+
{item.event.message}
|
|
194
|
+
</span>
|
|
195
|
+
{item.event.hotReload === true && (
|
|
196
|
+
<span className="shrink-0 px-1 rounded-sm bg-surface-2 text-text-muted">
|
|
197
|
+
已重启
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
{item.durationMs !== null && (
|
|
201
|
+
<span
|
|
202
|
+
data-compile-duration
|
|
203
|
+
className="shrink-0 text-text-muted tabular-nums"
|
|
204
|
+
>
|
|
205
|
+
{(item.durationMs / 1000).toFixed(1)}s
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
</li>
|
|
209
|
+
) : (
|
|
210
|
+
<li
|
|
211
|
+
key={`l-${item.at}-${index}`}
|
|
212
|
+
data-compile-log
|
|
213
|
+
data-stream={item.log.stream}
|
|
214
|
+
className="flex items-baseline gap-2 leading-5"
|
|
215
|
+
>
|
|
216
|
+
<span className="text-text-muted tabular-nums shrink-0">
|
|
217
|
+
{formatTime(item.at)}
|
|
218
|
+
</span>
|
|
219
|
+
<span
|
|
220
|
+
className={cn(
|
|
221
|
+
'break-all font-mono',
|
|
222
|
+
item.log.stream === 'stderr' ? 'text-red-500' : 'text-text',
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{item.log.text}
|
|
226
|
+
</span>
|
|
227
|
+
</li>
|
|
228
|
+
),
|
|
229
|
+
)}
|
|
230
|
+
<div ref={bottomRef} />
|
|
231
|
+
</ul>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The host-transport contract behind the 编译 panel: the panel's data wiring
|
|
2
|
+
// (seed, live event/log appends with FIFO caps, visibility gating, clearing)
|
|
3
|
+
// is written ONCE against this interface (see ConnectedCompilePanel); each
|
|
4
|
+
// host only implements how the operations travel — Electron IPC pushes, an
|
|
5
|
+
// in-page compile-service callback, or anything else.
|
|
6
|
+
import type { CompileEvent, CompileLogEntry } from './compile-types.js'
|
|
7
|
+
|
|
8
|
+
/** The seed payload: both stores, chronological (oldest first). Cross-stream
|
|
9
|
+
* order between an event and a log sharing the same millisecond `at` is only
|
|
10
|
+
* preserved when the host stamps `seq` itself — an unstamped snapshot gets
|
|
11
|
+
* `seq` assigned per store (events first), so such ties sort event-first. */
|
|
12
|
+
export interface CompileFeedSnapshot {
|
|
13
|
+
events: CompileEvent[]
|
|
14
|
+
logs: CompileLogEntry[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One live push: a status event, a log line, or a host-side history reset. */
|
|
18
|
+
export type CompileFeedEvent =
|
|
19
|
+
| { kind: 'event'; event: CompileEvent }
|
|
20
|
+
| { kind: 'log'; log: CompileLogEntry }
|
|
21
|
+
| { kind: 'reset' }
|
|
22
|
+
|
|
23
|
+
export interface CompilePanelSource {
|
|
24
|
+
/** Fetch the current feed history (seed on panel activation). */
|
|
25
|
+
getSnapshot(): Promise<CompileFeedSnapshot>
|
|
26
|
+
/** Live feed pushes; returns an unsubscribe function. */
|
|
27
|
+
subscribe(onEvent: (evt: CompileFeedEvent) => void): () => void
|
|
28
|
+
/** Visibility gate: hosts whose feed costs something only keep it armed
|
|
29
|
+
* while some panel is visible. */
|
|
30
|
+
setActive(on: boolean): void
|
|
31
|
+
/** Clear the host-side history. Optional: the panel's 清空 empties its
|
|
32
|
+
* local timeline regardless; hosts that keep no history of their own can
|
|
33
|
+
* omit this. */
|
|
34
|
+
clear?(): void | Promise<void>
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The compile panel's two feed item shapes. The stores never cross: status
|
|
2
|
+
// events come from the host's compile-status feed, per-line compiler output
|
|
3
|
+
// lands in the log feed — merging them is a view concern.
|
|
4
|
+
|
|
5
|
+
/** One entry of the 编译 tab's event log (a compile-status transition). */
|
|
6
|
+
export interface CompileEvent {
|
|
7
|
+
/** Wall-clock capture time (Date.now) of the payload's arrival. */
|
|
8
|
+
at: number
|
|
9
|
+
status: string
|
|
10
|
+
message: string
|
|
11
|
+
/** True when the payload came from a watcher rebuild (热更新 chip). */
|
|
12
|
+
hotReload?: boolean
|
|
13
|
+
/**
|
|
14
|
+
* Optional shared monotonic arrival counter spanning compile events AND
|
|
15
|
+
* compile logs — the panel's same-`at` tie-break: `at` is a
|
|
16
|
+
* millisecond stamp, so an event and the log lines of the same compile
|
|
17
|
+
* routinely collide on it.
|
|
18
|
+
*/
|
|
19
|
+
seq?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One per-line compiler log entry. `at` is the capture timestamp. */
|
|
23
|
+
export interface CompileLogEntry {
|
|
24
|
+
at: number
|
|
25
|
+
stream: 'stdout' | 'stderr'
|
|
26
|
+
text: string
|
|
27
|
+
/**
|
|
28
|
+
* Optional shared monotonic arrival counter spanning compile EVENTS and
|
|
29
|
+
* LOGS. `at` is a millisecond stamp, so a status event and the log lines
|
|
30
|
+
* of the same compile routinely collide on the same `at` — the panel uses
|
|
31
|
+
* `seq` as the same-`at` tie-break so the merged timeline keeps true
|
|
32
|
+
* arrival order.
|
|
33
|
+
*/
|
|
34
|
+
seq?: number
|
|
35
|
+
}
|