@dimina-kit/inspect 0.3.0 → 0.4.0-dev.20260716153350
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 +46 -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 +32 -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
package/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# @dimina-kit/inspect
|
|
2
2
|
|
|
3
3
|
Host-agnostic runtime inspection for dimina mini-programs: WXML tree
|
|
4
|
-
extraction
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
extraction, Storage inspection, AppData (page `setData` state) and the 编译
|
|
5
|
+
(compile) timeline. One package owns the protocol types, the pure logic
|
|
6
|
+
(Vue-runtime walk, stable-id registry, mutation-observing inspector,
|
|
7
|
+
storage-event reduction, setData accumulation), the React panels, and the
|
|
8
|
+
panels' data wiring — so the Electron devtools and any downstream host
|
|
9
|
+
(browser workbench, preview iframe) share a single implementation and stay
|
|
10
|
+
wire-compatible.
|
|
9
11
|
|
|
10
12
|
## Entry points
|
|
11
13
|
|
|
12
|
-
- `@dimina-kit/inspect` — core
|
|
14
|
+
- `@dimina-kit/inspect` — core (no runtime dependencies; the JSON tree viewer
|
|
15
|
+
is pulled in by the `/panel` entry only):
|
|
13
16
|
- `WxmlNode` / `ElementInspection` — the wire-format types. Hosts transport
|
|
14
17
|
them over IPC, `postMessage` or anything else.
|
|
15
18
|
- `walkInstance(instance, depth)` — walks a mounted dimina render-layer Vue
|
|
@@ -24,14 +27,26 @@ preview iframe) share a single implementation and stay wire-compatible.
|
|
|
24
27
|
- `StorageItem` / `StorageEvent` / `StorageWriteResult` — the Storage
|
|
25
28
|
wire-format types, plus `applyStorageEvent(items, evt)`, the pure
|
|
26
29
|
reducer that folds a change feed into an item list.
|
|
30
|
+
- `AppDataAccumulator` + `decodeWorkerMessage` / `decodeOutgoingMessage` /
|
|
31
|
+
`decodedToInput` — the cumulative per-(bridge, module) `setData` state
|
|
32
|
+
behind the AppData panel. Hosts tap the dimina service→render message
|
|
33
|
+
stream wherever they can reach it (an Electron preload sniffing Worker
|
|
34
|
+
`message` events, a same-origin workbench observing the pageFrame's
|
|
35
|
+
Worker) and feed this one accumulator, so the decode/merge/page-only
|
|
36
|
+
policy can't drift between hosts. `AppDataSnapshot` is the wire format.
|
|
37
|
+
- `CompileEvent` / `CompileLogEntry` — the 编译 panel's two feed item
|
|
38
|
+
shapes (status transitions and per-line compiler output).
|
|
27
39
|
- `@dimina-kit/inspect/panel` — the React layer (React ≥ 18 peer):
|
|
28
|
-
- `WxmlPanel` / `StoragePanel`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
- `WxmlPanel` / `StoragePanel` / `AppDataPanel` / `CompilePanel` — the pure
|
|
41
|
+
views (props in, no data wiring).
|
|
42
|
+
- `ConnectedWxmlPanel` / `ConnectedStoragePanel` / `ConnectedAppDataPanel` /
|
|
43
|
+
`ConnectedCompilePanel` — the panels' data wiring, written once against
|
|
44
|
+
their source contracts: seed on the (enabled && active) rising edge, live
|
|
45
|
+
updates via the push subscription, visibility gating, plus the
|
|
46
|
+
panel-specific parts — hover inspection (WXML), write forwarding
|
|
47
|
+
(Storage), bridge-tab auto-follow of the active page (AppData), FIFO caps
|
|
48
|
+
and arrival-order `seq` stamping (编译). Hosts render them with their
|
|
49
|
+
source implementation and never duplicate the wiring.
|
|
35
50
|
- Styling uses Tailwind utility classes over CSS variables
|
|
36
51
|
(`--color-code-blue`, `--color-surface-2`, …); the consuming app provides
|
|
37
52
|
the Tailwind theme mapping and variable values, and must include this
|
|
@@ -45,6 +60,15 @@ preview iframe) share a single implementation and stay wire-compatible.
|
|
|
45
60
|
`clear` / `clearAll?` / `getPrefix`. `clearAll` is optional — hosts whose
|
|
46
61
|
storage partition is shared with non-mini-program data must omit it, and
|
|
47
62
|
the panel then hides the origin-wide wipe entirely.
|
|
63
|
+
- `AppDataPanelSource` (main entry, type-only) — the AppData counterpart:
|
|
64
|
+
`getSnapshot` / `subscribe` / `setActive`. Pushes carry the FULL cumulative
|
|
65
|
+
`AppDataSnapshot` (merging patches is the producer-side accumulator's job).
|
|
66
|
+
- `CompilePanelSource` (main entry, type-only) — the 编译 counterpart:
|
|
67
|
+
`getSnapshot` / `subscribe` / `setActive` / `clear?`. The subscription
|
|
68
|
+
pushes `CompileFeedEvent`s (`event` / `log` appends or a host-side
|
|
69
|
+
`reset`); the connected panel owns the FIFO caps (200 events / 300 logs)
|
|
70
|
+
and stamps a shared monotonic `seq` onto unstamped arrivals so same-`at`
|
|
71
|
+
ties keep arrival order in the merged timeline.
|
|
48
72
|
|
|
49
73
|
## Contract notes
|
|
50
74
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic AppData accumulator.
|
|
3
|
+
*
|
|
4
|
+
* The dimina runtime emits two service→render messages relevant to the AppData
|
|
5
|
+
* panel, identical in shape on BOTH container paths:
|
|
6
|
+
* • update batches `{type:'ub', body:{bridgeId, updates:[{moduleId,data}]}}`
|
|
7
|
+
* — each `data` is a partial setData patch.
|
|
8
|
+
* • instance init: `type === 'page_*'`, body `{bridgeId, path, data}` with the
|
|
9
|
+
* COMPLETE initial state.
|
|
10
|
+
*
|
|
11
|
+
* Hosts differ only in where they tap that stream — an Electron preload
|
|
12
|
+
* sniffing Worker `message` events, a main-process service→render forward, or
|
|
13
|
+
* a same-origin web workbench observing the pageFrame's Worker. Every tap
|
|
14
|
+
* feeds this one accumulator so the decode/merge/page-only/init-gate policy
|
|
15
|
+
* can't drift between hosts.
|
|
16
|
+
*/
|
|
17
|
+
/** Parse a service/worker message payload; a non-string is passed through as-is. */
|
|
18
|
+
function parseMessagePayload(message) {
|
|
19
|
+
if (typeof message !== 'string')
|
|
20
|
+
return message;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(message);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decode a `ub` (update batch) body into patch entries, PAGE modules only —
|
|
30
|
+
* component-module updates are dropped (see `decodeWorkerMessage`).
|
|
31
|
+
*/
|
|
32
|
+
function decodeUpdateBatchBody(rawBody) {
|
|
33
|
+
const body = rawBody;
|
|
34
|
+
if (!body || typeof body !== 'object')
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof body.bridgeId !== 'string' || !Array.isArray(body.updates))
|
|
37
|
+
return null;
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const u of body.updates) {
|
|
40
|
+
if (!u || typeof u.moduleId !== 'string')
|
|
41
|
+
continue;
|
|
42
|
+
if (!u.moduleId.startsWith('page_'))
|
|
43
|
+
continue;
|
|
44
|
+
out.push({ mode: 'patch', bridgeId: body.bridgeId, moduleId: u.moduleId, data: u.data });
|
|
45
|
+
}
|
|
46
|
+
return out.length > 0 ? out : null;
|
|
47
|
+
}
|
|
48
|
+
/** Decode a `page_*` instance-init body into its single init entry. */
|
|
49
|
+
function decodePageInitBody(moduleId, rawBody) {
|
|
50
|
+
const body = rawBody;
|
|
51
|
+
if (!body || typeof body !== 'object')
|
|
52
|
+
return null;
|
|
53
|
+
if (typeof body.bridgeId !== 'string' || typeof body.path !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
if (!body.data || typeof body.data !== 'object')
|
|
56
|
+
return null;
|
|
57
|
+
return [{
|
|
58
|
+
mode: 'init',
|
|
59
|
+
bridgeId: body.bridgeId,
|
|
60
|
+
moduleId,
|
|
61
|
+
componentPath: body.path,
|
|
62
|
+
data: body.data,
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Decode a service→render message into AppData entries, or null when it is not
|
|
67
|
+
* AppData-relevant. Policy: surface PAGE entries only — component entries are
|
|
68
|
+
* dropped (they sometimes flow on a bridge id distinct from their owning page's
|
|
69
|
+
* and never receive pageUnload, which would manifest as ghost tabs).
|
|
70
|
+
*/
|
|
71
|
+
export function decodeWorkerMessage(message) {
|
|
72
|
+
const payload = parseMessagePayload(message);
|
|
73
|
+
if (!payload || typeof payload !== 'object')
|
|
74
|
+
return null;
|
|
75
|
+
const record = payload;
|
|
76
|
+
if (record.type === 'ub')
|
|
77
|
+
return decodeUpdateBatchBody(record.body);
|
|
78
|
+
if (typeof record.type === 'string' && record.type.startsWith('page_')) {
|
|
79
|
+
return decodePageInitBody(record.type, record.body);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/** main→worker direction: container signals page teardown so cache can evict. */
|
|
84
|
+
export function decodeOutgoingMessage(message) {
|
|
85
|
+
const payload = parseMessagePayload(message);
|
|
86
|
+
if (!payload || typeof payload !== 'object')
|
|
87
|
+
return null;
|
|
88
|
+
const r = payload;
|
|
89
|
+
if (typeof r.type !== 'string')
|
|
90
|
+
return null;
|
|
91
|
+
return {
|
|
92
|
+
type: r.type,
|
|
93
|
+
bridgeId: typeof r.body?.bridgeId === 'string' ? r.body.bridgeId : undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Convert a decoded entry to the normalized accumulator input. */
|
|
97
|
+
export function decodedToInput(entry) {
|
|
98
|
+
const input = {
|
|
99
|
+
bridgeId: entry.bridgeId,
|
|
100
|
+
moduleId: entry.moduleId,
|
|
101
|
+
data: entry.data,
|
|
102
|
+
mode: entry.mode,
|
|
103
|
+
};
|
|
104
|
+
if (entry.mode === 'init')
|
|
105
|
+
input.componentPath = entry.componentPath;
|
|
106
|
+
return input;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Cumulative per-(bridgeId, moduleId) setData state. Pure data structure — no
|
|
110
|
+
* Worker / IPC / DOM. Callers own transport (publish/emit) and the automation
|
|
111
|
+
* mirror.
|
|
112
|
+
*/
|
|
113
|
+
export class AppDataAccumulator {
|
|
114
|
+
cache = new Map();
|
|
115
|
+
// Bridges in insertion order — drives the `bridges` array (stable tab order).
|
|
116
|
+
bridgeOrder = [];
|
|
117
|
+
// Page path per bridge: set from `page_*` init's body.path (the page route).
|
|
118
|
+
bridgePagePath = new Map();
|
|
119
|
+
recordBridge(bridgeId) {
|
|
120
|
+
if (!this.bridgeOrder.includes(bridgeId))
|
|
121
|
+
this.bridgeOrder.push(bridgeId);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Apply one entry. Returns true if it was accepted (a mutation worth
|
|
125
|
+
* republishing), false if dropped (missing ids, or the init-gate).
|
|
126
|
+
*/
|
|
127
|
+
apply(input) {
|
|
128
|
+
if (!input.bridgeId || !input.moduleId)
|
|
129
|
+
return false;
|
|
130
|
+
// Drop ub patches whose bridge has never been initialised. dimina
|
|
131
|
+
// dispatches pageUnload → onUnload, whose setData produces a late `ub`
|
|
132
|
+
// arriving AFTER clearBridge; without this gate it would resurrect the
|
|
133
|
+
// unloaded bridge as a ghost tab.
|
|
134
|
+
if (input.mode !== 'init' && !this.bridgePagePath.has(input.bridgeId))
|
|
135
|
+
return false;
|
|
136
|
+
const key = `${input.bridgeId}/${input.moduleId}`;
|
|
137
|
+
const prev = this.cache.get(key);
|
|
138
|
+
const incoming = input.data && typeof input.data === 'object'
|
|
139
|
+
? input.data
|
|
140
|
+
: {};
|
|
141
|
+
// init = full-state replace; patch = merge into previous (setData semantics)
|
|
142
|
+
const merged = input.mode === 'init'
|
|
143
|
+
? { ...incoming }
|
|
144
|
+
: { ...(prev?.data ?? {}), ...incoming };
|
|
145
|
+
const componentPath = input.componentPath ?? prev?.componentPath;
|
|
146
|
+
const next = componentPath !== undefined
|
|
147
|
+
? { componentPath, data: merged }
|
|
148
|
+
: { data: merged };
|
|
149
|
+
this.cache.set(key, next);
|
|
150
|
+
this.recordBridge(input.bridgeId);
|
|
151
|
+
if (input.mode === 'init' && input.moduleId.startsWith('page_') && input.componentPath) {
|
|
152
|
+
this.bridgePagePath.set(input.bridgeId, input.componentPath);
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/** Evict every entry for a bridge (page teardown). */
|
|
157
|
+
clearBridge(bridgeId) {
|
|
158
|
+
const prefix = `${bridgeId}/`;
|
|
159
|
+
for (const key of [...this.cache.keys()]) {
|
|
160
|
+
if (key.startsWith(prefix))
|
|
161
|
+
this.cache.delete(key);
|
|
162
|
+
}
|
|
163
|
+
const idx = this.bridgeOrder.indexOf(bridgeId);
|
|
164
|
+
if (idx >= 0)
|
|
165
|
+
this.bridgeOrder.splice(idx, 1);
|
|
166
|
+
this.bridgePagePath.delete(bridgeId);
|
|
167
|
+
}
|
|
168
|
+
/** The full cumulative snapshot for the panel. */
|
|
169
|
+
snapshot() {
|
|
170
|
+
const bridges = [];
|
|
171
|
+
for (const id of this.bridgeOrder) {
|
|
172
|
+
bridges.push({ id, pagePath: this.bridgePagePath.get(id) ?? null });
|
|
173
|
+
}
|
|
174
|
+
const entries = {};
|
|
175
|
+
for (const [key, entry] of this.cache) {
|
|
176
|
+
const slash = key.indexOf('/');
|
|
177
|
+
if (slash < 0)
|
|
178
|
+
continue;
|
|
179
|
+
const bridgeId = key.slice(0, slash);
|
|
180
|
+
const moduleId = key.slice(slash + 1);
|
|
181
|
+
if (!entries[bridgeId])
|
|
182
|
+
entries[bridgeId] = {};
|
|
183
|
+
const displayKey = entry.componentPath ?? moduleId;
|
|
184
|
+
entries[bridgeId][displayKey] = entry.data;
|
|
185
|
+
}
|
|
186
|
+
return { bridges, entries };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The current reactive page state for a bridge: shallow-merge of `entry.data`
|
|
190
|
+
* across every cache entry whose key starts with `${bridgeId}/`, in insertion
|
|
191
|
+
* order (later entries win on key conflicts). `{}` when no entries match
|
|
192
|
+
* (unknown bridge / after clearBridge). Pure, no side effects.
|
|
193
|
+
*/
|
|
194
|
+
pageData(bridgeId) {
|
|
195
|
+
const merged = {};
|
|
196
|
+
for (const [key, entry] of this.cache) {
|
|
197
|
+
const slash = key.indexOf('/');
|
|
198
|
+
if (slash < 0)
|
|
199
|
+
continue;
|
|
200
|
+
if (key.slice(0, slash) !== bridgeId)
|
|
201
|
+
continue;
|
|
202
|
+
Object.assign(merged, entry.data);
|
|
203
|
+
}
|
|
204
|
+
return merged;
|
|
205
|
+
}
|
|
206
|
+
/** Flat `key → data` map for the `__simulatorData.getAppdata()` mirror. */
|
|
207
|
+
flat() {
|
|
208
|
+
const data = {};
|
|
209
|
+
for (const [key, entry] of this.cache)
|
|
210
|
+
data[key] = entry.data;
|
|
211
|
+
return data;
|
|
212
|
+
}
|
|
213
|
+
clear() {
|
|
214
|
+
this.cache.clear();
|
|
215
|
+
this.bridgeOrder.length = 0;
|
|
216
|
+
this.bridgePagePath.clear();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as jsonViewModule from '@uiw/react-json-view';
|
|
3
|
+
const moduleDefault = jsonViewModule.default;
|
|
4
|
+
// A type predicate (not a bare `in` check): plain `in` narrowing intersects
|
|
5
|
+
// `Record<'default', unknown>` onto the component branch, degrading the
|
|
6
|
+
// conditional's type to unknown.
|
|
7
|
+
function isInteropNamespace(m) {
|
|
8
|
+
return 'default' in m;
|
|
9
|
+
}
|
|
10
|
+
const JsonView = isInteropNamespace(moduleDefault) ? moduleDefault.default : moduleDefault;
|
|
11
|
+
function bridgeLabel(bridge) {
|
|
12
|
+
return bridge.pagePath ?? bridge.id;
|
|
13
|
+
}
|
|
14
|
+
// Map the json-view CSS vars onto the panel's existing design tokens so the
|
|
15
|
+
// component blends with both dark and light themes. CSS variables defined
|
|
16
|
+
// here cascade to the json-view's internal vars (--w-rjv-*) via inline style.
|
|
17
|
+
const JSON_VIEW_STYLE = {
|
|
18
|
+
'--w-rjv-font-family': 'var(--font-family-mono)',
|
|
19
|
+
'--w-rjv-background-color': 'transparent',
|
|
20
|
+
'--w-rjv-color': 'var(--color-code-blue)',
|
|
21
|
+
'--w-rjv-key-string': 'var(--color-code-blue)',
|
|
22
|
+
'--w-rjv-line-color': 'var(--color-border-subtle)',
|
|
23
|
+
'--w-rjv-arrow-color': 'var(--color-text-secondary)',
|
|
24
|
+
'--w-rjv-info-color': 'var(--color-code-label)',
|
|
25
|
+
'--w-rjv-curlybraces-color': 'var(--color-text-secondary)',
|
|
26
|
+
'--w-rjv-brackets-color': 'var(--color-text-secondary)',
|
|
27
|
+
'--w-rjv-quotes-color': 'var(--color-code-orange)',
|
|
28
|
+
'--w-rjv-quotes-string-color': 'var(--color-code-orange)',
|
|
29
|
+
'--w-rjv-type-string-color': 'var(--color-code-orange)',
|
|
30
|
+
'--w-rjv-type-int-color': 'var(--color-code-number)',
|
|
31
|
+
'--w-rjv-type-float-color': 'var(--color-code-number)',
|
|
32
|
+
'--w-rjv-type-bigint-color': 'var(--color-code-number)',
|
|
33
|
+
'--w-rjv-type-boolean-color': 'var(--color-code-keyword)',
|
|
34
|
+
'--w-rjv-type-null-color': 'var(--color-code-keyword)',
|
|
35
|
+
'--w-rjv-type-nan-color': 'var(--color-code-keyword)',
|
|
36
|
+
'--w-rjv-type-undefined-color': 'var(--color-code-keyword)',
|
|
37
|
+
'--w-rjv-type-date-color': 'var(--color-code-label)',
|
|
38
|
+
'--w-rjv-type-url-color': 'var(--color-code-blue)',
|
|
39
|
+
fontSize: 12,
|
|
40
|
+
padding: '6px 8px',
|
|
41
|
+
wordBreak: 'break-all',
|
|
42
|
+
whiteSpace: 'pre-wrap',
|
|
43
|
+
};
|
|
44
|
+
export function AppDataPanel({ state, onSelectBridge, isRuntimeRunning = true, }) {
|
|
45
|
+
const { bridges, activeBridgeId, entries } = state;
|
|
46
|
+
return (_jsxs("div", { className: "flex flex-col overflow-hidden flex-1", "data-testid": "appdata-panel", children: [bridges.length > 1 && (_jsx("div", { className: "flex gap-1 px-2 py-1 border-b border-border-subtle shrink-0 overflow-x-auto bg-bg-panel", children: bridges.map((b) => {
|
|
47
|
+
const isActive = b.id === activeBridgeId;
|
|
48
|
+
return (_jsx("button", { onClick: () => onSelectBridge(b.id), title: b.id, className: 'shrink-0 px-2 py-0.5 text-[11px] rounded border transition-colors '
|
|
49
|
+
+ (isActive
|
|
50
|
+
? 'border-accent text-accent bg-surface-3'
|
|
51
|
+
: 'border-border-subtle text-text-dim hover:border-accent hover:text-accent'), children: bridgeLabel(b) }, b.id));
|
|
52
|
+
}) })), bridges.length === 0 ? (_jsx("div", { className: "text-[12px] text-text-dim text-center px-4 py-6", children: isRuntimeRunning ? '暂无页面数据(仅显示 Page 级 data)' : '小程序未运行' })) : (
|
|
53
|
+
// A bridge with zero entries still gets its keepalive container (with
|
|
54
|
+
// the per-bridge empty text) so live pushes land in a mounted tab.
|
|
55
|
+
// Keepalive: render every bridge's entries; hide non-active ones via
|
|
56
|
+
// `display: none` so the JsonView instances stay mounted and preserve
|
|
57
|
+
// their expand/collapse state across tab switches.
|
|
58
|
+
_jsx("div", { className: "flex-1 overflow-hidden relative", children: bridges.map((b) => {
|
|
59
|
+
const isActive = b.id === activeBridgeId;
|
|
60
|
+
const bridgeEntries = entries[b.id] ?? {};
|
|
61
|
+
const keys = Object.keys(bridgeEntries);
|
|
62
|
+
return (_jsx("div", { "data-bridge-id": b.id, className: "absolute inset-0 flex flex-col gap-2 p-2 overflow-y-auto", style: { display: isActive ? 'flex' : 'none' }, children: keys.length === 0 ? (_jsx("div", { className: "text-[12px] text-text-dim text-center px-4 py-6", children: "\u6682\u65E0\u9875\u9762\u6570\u636E\uFF08\u4EC5\u663E\u793A Page \u7EA7 data\uFF09" })) : (keys.map((comp) => (_jsxs("div", { className: "border border-border-subtle rounded overflow-hidden shrink-0", children: [_jsx("div", { className: "bg-surface-3 px-2 py-0.5 text-[11px] text-code-label truncate", children: comp }), _jsx(JsonView, { value: (bridgeEntries[comp] ?? {}), collapsed: 1, displayDataTypes: false, displayObjectSize: false, enableClipboard: false, indentWidth: 12, style: JSON_VIEW_STYLE })] }, `${b.id}::${comp}`)))) }, b.id));
|
|
63
|
+
}) }))] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The pure 编译 panel view: the host's compile-status events and per-line
|
|
3
|
+
// compiler log merged — in the VIEW layer only — into one oldest-first
|
|
4
|
+
// (chronological) timeline with a text filter. Pure presentation — feed
|
|
5
|
+
// state lives in the connected container (or the host, when it renders this
|
|
6
|
+
// view directly).
|
|
7
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
/** Join the truthy class fragments (the panel's only class-composition need). */
|
|
9
|
+
function cn(...parts) {
|
|
10
|
+
return parts.filter(Boolean).join(' ');
|
|
11
|
+
}
|
|
12
|
+
function formatTime(at) {
|
|
13
|
+
const d = new Date(at);
|
|
14
|
+
return [d.getHours(), d.getMinutes(), d.getSeconds()]
|
|
15
|
+
.map((n) => String(n).padStart(2, '0'))
|
|
16
|
+
.join(':');
|
|
17
|
+
}
|
|
18
|
+
/** Status → row/badge accent. Unknown statuses fall back to muted text. */
|
|
19
|
+
function statusClass(status) {
|
|
20
|
+
if (status === 'error')
|
|
21
|
+
return 'text-red-500';
|
|
22
|
+
if (status === 'compiling')
|
|
23
|
+
return 'text-amber-500';
|
|
24
|
+
if (status === 'ready')
|
|
25
|
+
return 'text-emerald-600';
|
|
26
|
+
return 'text-text-muted';
|
|
27
|
+
}
|
|
28
|
+
/** Extract the display text from a timeline item for filter matching. */
|
|
29
|
+
function itemText(item) {
|
|
30
|
+
return item.kind === 'event' ? item.event.message : item.log.text;
|
|
31
|
+
}
|
|
32
|
+
export function CompilePanel({ events, logs = [], onClear }) {
|
|
33
|
+
const latest = events.length > 0 ? events[events.length - 1] : null;
|
|
34
|
+
const [filter, setFilter] = useState('');
|
|
35
|
+
const scrollRef = useRef(null);
|
|
36
|
+
const bottomRef = useRef(null);
|
|
37
|
+
const shouldAutoScroll = useRef(true);
|
|
38
|
+
// Track whether the user has scrolled away from the bottom.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const container = scrollRef.current;
|
|
41
|
+
if (!container)
|
|
42
|
+
return;
|
|
43
|
+
const handleScroll = () => {
|
|
44
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
45
|
+
shouldAutoScroll.current = scrollHeight - scrollTop - clientHeight < 20;
|
|
46
|
+
};
|
|
47
|
+
container.addEventListener('scroll', handleScroll);
|
|
48
|
+
return () => container.removeEventListener('scroll', handleScroll);
|
|
49
|
+
}, []);
|
|
50
|
+
const totalItems = events.length + logs.length;
|
|
51
|
+
// Auto-scroll to bottom when new items arrive. Optional-call: jsdom-class
|
|
52
|
+
// environments don't implement scrollIntoView.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (shouldAutoScroll.current) {
|
|
55
|
+
bottomRef.current?.scrollIntoView?.({ behavior: 'auto' });
|
|
56
|
+
}
|
|
57
|
+
}, [totalItems]);
|
|
58
|
+
const timeline = useMemo(() => {
|
|
59
|
+
const items = [];
|
|
60
|
+
events.forEach((event, index) => {
|
|
61
|
+
// Duration pairs strictly with the IMMEDIATELY preceding event: a
|
|
62
|
+
// ready right after a compiling shows the elapsed compile time; any
|
|
63
|
+
// intervening event (e.g. an error) breaks the pair.
|
|
64
|
+
const previous = index > 0 ? events[index - 1] : null;
|
|
65
|
+
const durationMs = event.status === 'ready' && previous?.status === 'compiling'
|
|
66
|
+
? event.at - previous.at
|
|
67
|
+
: null;
|
|
68
|
+
items.push({ kind: 'event', at: event.at, seq: event.seq, event, durationMs });
|
|
69
|
+
});
|
|
70
|
+
for (const log of logs) {
|
|
71
|
+
items.push({ kind: 'log', at: log.at, seq: log.seq, log });
|
|
72
|
+
}
|
|
73
|
+
// Oldest first (chronological). Within a same-`at` tie (millisecond
|
|
74
|
+
// stamps — an event and the logs of the same compile collide routinely)
|
|
75
|
+
// the shared monotonic `seq` keeps ARRIVAL order.
|
|
76
|
+
return items.sort((a, b) => {
|
|
77
|
+
if (a.at !== b.at)
|
|
78
|
+
return a.at - b.at;
|
|
79
|
+
if (a.seq !== undefined && b.seq !== undefined)
|
|
80
|
+
return a.seq - b.seq;
|
|
81
|
+
return 0;
|
|
82
|
+
});
|
|
83
|
+
}, [events, logs]);
|
|
84
|
+
const filtered = useMemo(() => {
|
|
85
|
+
if (!filter)
|
|
86
|
+
return timeline;
|
|
87
|
+
const lower = filter.toLowerCase();
|
|
88
|
+
return timeline.filter((item) => itemText(item).toLowerCase().includes(lower));
|
|
89
|
+
}, [timeline, filter]);
|
|
90
|
+
const isEmpty = events.length === 0 && logs.length === 0;
|
|
91
|
+
return (_jsxs("div", { className: "flex flex-col h-full w-full min-h-0 text-[12px]", children: [_jsxs("div", { className: "flex items-center gap-2 px-2 h-8 shrink-0 border-b border-border-subtle", children: [latest ? (_jsx("span", { "data-compile-current": true, "data-status": latest.status, className: cn('truncate font-medium shrink-0', statusClass(latest.status)), children: latest.message })) : (_jsx("span", { className: "text-text-muted shrink-0", children: "\u7F16\u8BD1\u4FE1\u606F" })), _jsx("div", { className: "flex-1 min-w-0" }), _jsx("input", { type: "text", value: filter, onChange: (e) => setFilter(e.target.value), placeholder: "\u8FC7\u6EE4...", className: "h-6 px-2 text-[12px] rounded-sm border border-border-subtle bg-surface\n text-text placeholder:text-text-dim\n focus:outline-none focus:border-text-muted\n w-40 shrink-0" }), filter && (_jsxs("span", { className: "text-[11px] text-text-muted tabular-nums shrink-0", children: [filtered.length, "/", timeline.length] })), _jsx("button", { type: "button", onClick: onClear, className: "px-2 h-6 rounded-sm text-text-muted hover:text-text hover:bg-surface-2 transition-colors shrink-0", children: "\u6E05\u7A7A" })] }), _jsx("div", { ref: scrollRef, className: "flex-1 min-h-0 overflow-auto", children: isEmpty ? (_jsx("div", { className: "flex items-center justify-center h-full text-text-muted", children: "\u6682\u65E0\u7F16\u8BD1\u4FE1\u606F" })) : filtered.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full text-text-muted", children: "\u65E0\u5339\u914D\u65E5\u5FD7" })) : (_jsxs("ul", { className: "px-2 py-1 space-y-0.5", children: [filtered.map((item, index) => item.kind === 'event' ? (_jsxs("li", { "data-compile-row": true, "data-status": item.event.status, className: "flex items-baseline gap-2 leading-5", children: [_jsx("span", { className: "text-text-muted tabular-nums shrink-0", children: formatTime(item.at) }), _jsx("span", { className: cn('break-all', statusClass(item.event.status)), children: item.event.message }), item.event.hotReload === true && (_jsx("span", { className: "shrink-0 px-1 rounded-sm bg-surface-2 text-text-muted", children: "\u5DF2\u91CD\u542F" })), item.durationMs !== null && (_jsxs("span", { "data-compile-duration": true, className: "shrink-0 text-text-muted tabular-nums", children: [(item.durationMs / 1000).toFixed(1), "s"] }))] }, `e-${item.at}-${index}`)) : (_jsxs("li", { "data-compile-log": true, "data-stream": item.log.stream, className: "flex items-baseline gap-2 leading-5", children: [_jsx("span", { className: "text-text-muted tabular-nums shrink-0", children: formatTime(item.at) }), _jsx("span", { className: cn('break-all font-mono', item.log.stream === 'stderr' ? 'text-red-500' : 'text-text'), children: item.log.text })] }, `l-${item.at}-${index}`))), _jsx("div", { ref: bottomRef })] })) })] }));
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The AppData panel's data wiring, written once against AppDataPanelSource:
|
|
3
|
+
// seed on the (enabled && active) rising edge, stay live by replacing state
|
|
4
|
+
// with each pushed full snapshot, forward the visibility gate, and own the
|
|
5
|
+
// bridge-tab selection (auto-follow of the simulator's active page + manual
|
|
6
|
+
// picks). Hosts render this with their transport implementation (Electron
|
|
7
|
+
// IPC, a same-origin Worker tap, …) and the pure AppDataPanel view underneath
|
|
8
|
+
// never needs host-specific code.
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { AppDataPanel } from './appdata-panel-view.js';
|
|
11
|
+
import { useActiveBridgeId } from './use-active-bridge-id.js';
|
|
12
|
+
import { useSourceWiring } from './use-source-wiring.js';
|
|
13
|
+
const EMPTY_SNAPSHOT = { bridges: [], entries: {} };
|
|
14
|
+
export function ConnectedAppDataPanel({ source, active = true, enabled = true, isRuntimeRunning = true, activePagePath = '', }) {
|
|
15
|
+
const [snapshot, setSnapshot] = useState(EMPTY_SNAPSHOT);
|
|
16
|
+
useSourceWiring({
|
|
17
|
+
source,
|
|
18
|
+
enabled,
|
|
19
|
+
active,
|
|
20
|
+
subscribe: s => s.subscribe((next) => {
|
|
21
|
+
setSnapshot(next);
|
|
22
|
+
}),
|
|
23
|
+
seed: (s, isDisposed) => {
|
|
24
|
+
void s.getSnapshot().then((next) => {
|
|
25
|
+
if (!isDisposed())
|
|
26
|
+
setSnapshot(next);
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const { activeBridgeId, setActiveBridge } = useActiveBridgeId(snapshot.bridges, activePagePath);
|
|
31
|
+
return (_jsx(AppDataPanel, { state: { bridges: snapshot.bridges, activeBridgeId, entries: snapshot.entries }, onSelectBridge: setActiveBridge, isRuntimeRunning: isRuntimeRunning }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The 编译 panel's data wiring, written once against CompilePanelSource: seed
|
|
3
|
+
// on the (enabled && active) rising edge, stay live by appending pushed
|
|
4
|
+
// events/logs under FIFO caps, stamp a shared monotonic `seq` onto unstamped
|
|
5
|
+
// arrivals (the merged timeline's same-`at` tie-break), forward the
|
|
6
|
+
// visibility gate, and route 清空. Hosts render this with their transport
|
|
7
|
+
// implementation and the pure CompilePanel view underneath never needs
|
|
8
|
+
// host-specific code.
|
|
9
|
+
import { useRef, useState } from 'react';
|
|
10
|
+
import { CompilePanel } from './compile-panel-view.js';
|
|
11
|
+
import { useSourceWiring } from './use-source-wiring.js';
|
|
12
|
+
/** events cap — FIFO, oldest evicted first. */
|
|
13
|
+
const MAX_COMPILE_EVENTS = 200;
|
|
14
|
+
/** logs cap — FIFO, oldest evicted first. */
|
|
15
|
+
const MAX_COMPILE_LOGS = 300;
|
|
16
|
+
const EMPTY_FEED = { events: [], logs: [] };
|
|
17
|
+
export function ConnectedCompilePanel({ source, active = true, enabled = true, }) {
|
|
18
|
+
const [feed, setFeed] = useState(EMPTY_FEED);
|
|
19
|
+
// The shared arrival counter behind `seq` stamping. Ratcheted past every
|
|
20
|
+
// explicit seq seen so a locally stamped item can never undercut one the
|
|
21
|
+
// host stamped itself (which would silently reverse arrival order on a
|
|
22
|
+
// same-`at` tie).
|
|
23
|
+
const nextSeq = useRef(0);
|
|
24
|
+
const stamp = (item) => {
|
|
25
|
+
if (item.seq !== undefined) {
|
|
26
|
+
nextSeq.current = Math.max(nextSeq.current, item.seq + 1);
|
|
27
|
+
return item;
|
|
28
|
+
}
|
|
29
|
+
return { ...item, seq: nextSeq.current++ };
|
|
30
|
+
};
|
|
31
|
+
useSourceWiring({
|
|
32
|
+
source,
|
|
33
|
+
enabled,
|
|
34
|
+
active,
|
|
35
|
+
subscribe: s => s.subscribe((evt) => {
|
|
36
|
+
if (evt.kind === 'reset') {
|
|
37
|
+
setFeed(EMPTY_FEED);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (evt.kind === 'event') {
|
|
41
|
+
const event = stamp(evt.event);
|
|
42
|
+
setFeed(prev => ({
|
|
43
|
+
...prev,
|
|
44
|
+
events: [...prev.events, event].slice(-MAX_COMPILE_EVENTS),
|
|
45
|
+
}));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const log = stamp(evt.log);
|
|
49
|
+
setFeed(prev => ({
|
|
50
|
+
...prev,
|
|
51
|
+
logs: [...prev.logs, log].slice(-MAX_COMPILE_LOGS),
|
|
52
|
+
}));
|
|
53
|
+
}),
|
|
54
|
+
seed: (s, isDisposed) => {
|
|
55
|
+
void s.getSnapshot().then((snap) => {
|
|
56
|
+
if (isDisposed())
|
|
57
|
+
return;
|
|
58
|
+
setFeed({
|
|
59
|
+
events: snap.events.map(stamp).slice(-MAX_COMPILE_EVENTS),
|
|
60
|
+
logs: snap.logs.map(stamp).slice(-MAX_COMPILE_LOGS),
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const handleClear = () => {
|
|
66
|
+
// The local timeline empties regardless; clearing the host-side history
|
|
67
|
+
// is the optional capability.
|
|
68
|
+
setFeed(EMPTY_FEED);
|
|
69
|
+
void source.clear?.();
|
|
70
|
+
};
|
|
71
|
+
return _jsx(CompilePanel, { events: feed.events, logs: feed.logs, onClear: handleClear });
|
|
72
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { SYNTHETIC_SID_PREFIX, registerSyntheticSid, findElementBySid, } from '.
|
|
|
2
2
|
export { walkInstance } from './wxml-extract.js';
|
|
3
3
|
export { createWxmlInspector, } from './inspector.js';
|
|
4
4
|
export { applyStorageEvent } from './storage-reducer.js';
|
|
5
|
+
export { AppDataAccumulator, decodeWorkerMessage, decodeOutgoingMessage, decodedToInput, } from './appdata-accumulator.js';
|
package/dist/panel.js
CHANGED
|
@@ -5,3 +5,8 @@ export { WxmlPanel } from './panel-view.js';
|
|
|
5
5
|
export { ConnectedWxmlPanel } from './connected-panel.js';
|
|
6
6
|
export { StoragePanel } from './storage-panel-view.js';
|
|
7
7
|
export { ConnectedStoragePanel } from './connected-storage-panel.js';
|
|
8
|
+
export { AppDataPanel } from './appdata-panel-view.js';
|
|
9
|
+
export { ConnectedAppDataPanel } from './connected-appdata-panel.js';
|
|
10
|
+
export { useActiveBridgeId } from './use-active-bridge-id.js';
|
|
11
|
+
export { CompilePanel } from './compile-panel-view.js';
|
|
12
|
+
export { ConnectedCompilePanel } from './connected-compile-panel.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
function normalizePagePath(p) {
|
|
3
|
+
return (p ?? '').replace(/^\/+/, '');
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Derive which AppData page tab is active, synchronously.
|
|
7
|
+
*
|
|
8
|
+
* `activeBridgeId` is a pure derivation of (current bridges, the simulator's
|
|
9
|
+
* active page, manual pick), computed during render — so it is never stale for
|
|
10
|
+
* a frame the way a useEffect-derived value would be. Auto-follow tracks the
|
|
11
|
+
* page the user is actually looking at: when `activePagePath` matches a
|
|
12
|
+
* bridge's pagePath the panel snaps to it, so switching tabBar tabs (which
|
|
13
|
+
* re-inits no bridge) still moves the panel. A brand-new bridge id appearing or
|
|
14
|
+
* the active page changing both drop a prior manual pick back to auto-follow.
|
|
15
|
+
*/
|
|
16
|
+
export function useActiveBridgeId(bridges, activePagePath = '') {
|
|
17
|
+
const ids = bridges.map((b) => b.id);
|
|
18
|
+
// NUL separator: ids are runtime-generated and never contain it, so two
|
|
19
|
+
// distinct id lists can never collide on the same key.
|
|
20
|
+
const idsKey = ids.join('\x00');
|
|
21
|
+
// The user's manual tab pick; null means "auto-follow the active page".
|
|
22
|
+
const [selectedBridgeId, setSelectedBridgeId] = useState(null);
|
|
23
|
+
// The id list from the previous render — lets us spot a freshly-inited page.
|
|
24
|
+
const [prevIdsKey, setPrevIdsKey] = useState('');
|
|
25
|
+
// The active page from the previous render — lets us spot a tab switch.
|
|
26
|
+
const [prevActivePath, setPrevActivePath] = useState(activePagePath);
|
|
27
|
+
if (idsKey !== prevIdsKey) {
|
|
28
|
+
// The bridge set changed. If a never-before-seen id appeared, a new page
|
|
29
|
+
// inited → drop back to auto-follow. Adjusting state during render is the
|
|
30
|
+
// React-sanctioned alternative to an effect: React re-renders synchronously
|
|
31
|
+
// before the commit, so `activeBridgeId` below is correct this frame.
|
|
32
|
+
const prevIds = prevIdsKey ? prevIdsKey.split('\x00') : [];
|
|
33
|
+
if (ids.some((id) => !prevIds.includes(id))) {
|
|
34
|
+
setSelectedBridgeId(null);
|
|
35
|
+
}
|
|
36
|
+
setPrevIdsKey(idsKey);
|
|
37
|
+
}
|
|
38
|
+
if (activePagePath !== prevActivePath) {
|
|
39
|
+
// The simulator navigated to a different active page (e.g. a tabBar switch).
|
|
40
|
+
// Drop the manual pick so the panel re-follows the page on screen — without
|
|
41
|
+
// this, switching tabs leaves the panel stuck on whichever page inited last.
|
|
42
|
+
setSelectedBridgeId(null);
|
|
43
|
+
setPrevActivePath(activePagePath);
|
|
44
|
+
}
|
|
45
|
+
// Auto-follow target: the bridge whose pagePath matches the active page.
|
|
46
|
+
const activeFollowId = activePagePath
|
|
47
|
+
? bridges.find((b) => normalizePagePath(b.pagePath) === normalizePagePath(activePagePath))?.id ?? null
|
|
48
|
+
: null;
|
|
49
|
+
const activeBridgeId = selectedBridgeId && ids.includes(selectedBridgeId)
|
|
50
|
+
? selectedBridgeId
|
|
51
|
+
// Prefer the active page; fall back to the newest bridge when the active
|
|
52
|
+
// page is unknown or has no matching bridge yet.
|
|
53
|
+
: (activeFollowId ?? ids.at(-1) ?? null);
|
|
54
|
+
const setActiveBridge = (id) => {
|
|
55
|
+
if (ids.includes(id))
|
|
56
|
+
setSelectedBridgeId(id);
|
|
57
|
+
};
|
|
58
|
+
return { activeBridgeId, setActiveBridge };
|
|
59
|
+
}
|