@cfp-studio/iframe 18.0.1 → 18.3.0
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 -0
- package/dist/index.d.mts +168 -149
- package/dist/index.mjs +1 -1
- package/package.json +21 -9
- package/preview/__previewjs_internal__/index.ts +2 -3
- package/preview/__previewjs_internal__/links.ts +0 -1
- package/preview/__previewjs_internal__/logs.ts +0 -4
- package/preview/__previewjs_internal__/overlay.ts +187 -0
- package/preview/__previewjs_internal__/run-renderer.ts +4 -3
- package/preview/__previewjs_internal__/runtime-bridge.ts +559 -0
- package/preview/__previewjs_internal__/vscode-control.ts +75 -50
- package/preview/index.html +17 -5
- package/dist/index.d.ts +0 -268
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime bridge for the hidden component preview.
|
|
3
|
+
*
|
|
4
|
+
* It runs inside the preview iframe, reads React's dev source metadata from
|
|
5
|
+
* the fiber attached to each host DOM node, and posts a compact snapshot to the
|
|
6
|
+
* webview parent. It never changes the rendered tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type FiberLike,
|
|
11
|
+
genFiberRenderChain,
|
|
12
|
+
genFiberSourceChain,
|
|
13
|
+
getCodeInfoFromDebugSource,
|
|
14
|
+
getCodeInfoFromProps,
|
|
15
|
+
getDisplayNameForFiber,
|
|
16
|
+
getElementFiberUpward,
|
|
17
|
+
getFiberSourceLocation,
|
|
18
|
+
resolveFiberTagKind,
|
|
19
|
+
type RuntimeComponentFrame,
|
|
20
|
+
type RuntimeNode,
|
|
21
|
+
type RuntimeNodeDetail,
|
|
22
|
+
type RuntimeSnapshot,
|
|
23
|
+
summarizeComputedStyle,
|
|
24
|
+
type RuntimeSourceLocation,
|
|
25
|
+
} from "@cfp-studio/fiber-reader";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
clearPageOverlay,
|
|
29
|
+
isOverlayChrome,
|
|
30
|
+
setPageInteractionShield,
|
|
31
|
+
setPageOverlay,
|
|
32
|
+
type PageOverlayPayload,
|
|
33
|
+
} from "./overlay";
|
|
34
|
+
|
|
35
|
+
export type {
|
|
36
|
+
RuntimeBounds,
|
|
37
|
+
RuntimeComponentFrame,
|
|
38
|
+
RuntimeDomSummary,
|
|
39
|
+
RuntimeNode,
|
|
40
|
+
RuntimeNodeDetail,
|
|
41
|
+
RuntimeSnapshot,
|
|
42
|
+
RuntimeSourceLocation,
|
|
43
|
+
RuntimeViewport,
|
|
44
|
+
} from "@cfp-studio/fiber-reader";
|
|
45
|
+
|
|
46
|
+
declare global {
|
|
47
|
+
interface Window {
|
|
48
|
+
__cfpBridge?: (payload: string) => void;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MAX_NODES = 2000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 盾牌只能改命中目标,不能拦住默认行为:wheel 会在盾牌上继续冒泡成滚动,
|
|
56
|
+
* 拖拽/选区/上下文菜单也各有自己的默认动作。inspect 期间逐类拦掉。
|
|
57
|
+
*/
|
|
58
|
+
const BLOCKED_INPUT_EVENTS = [
|
|
59
|
+
"pointerdown",
|
|
60
|
+
"pointerup",
|
|
61
|
+
"pointercancel",
|
|
62
|
+
"mousedown",
|
|
63
|
+
"mouseup",
|
|
64
|
+
"dblclick",
|
|
65
|
+
"contextmenu",
|
|
66
|
+
"dragstart",
|
|
67
|
+
"selectstart",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
/** 键盘滚动/翻页:焦点已经 blur 掉,但这些键仍然会滚页面。 */
|
|
71
|
+
const SCROLL_KEYS = new Set([
|
|
72
|
+
" ",
|
|
73
|
+
"PageUp",
|
|
74
|
+
"PageDown",
|
|
75
|
+
"Home",
|
|
76
|
+
"End",
|
|
77
|
+
"ArrowUp",
|
|
78
|
+
"ArrowDown",
|
|
79
|
+
"ArrowLeft",
|
|
80
|
+
"ArrowRight",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
let initialized = false;
|
|
84
|
+
let inspectEnabled = false;
|
|
85
|
+
const nodeElements = new Map<string, Element>();
|
|
86
|
+
|
|
87
|
+
export function setupRuntimeBridge(): void {
|
|
88
|
+
if (initialized || typeof window === "undefined") {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
initialized = true;
|
|
92
|
+
window.addEventListener("message", (event) => {
|
|
93
|
+
if (event.data?.kind === "cfp:runtime-request") {
|
|
94
|
+
publishRuntimeSnapshot();
|
|
95
|
+
} else if (event.data?.kind === "cfp:set-inspect") {
|
|
96
|
+
setInspectEnabled(Boolean(event.data.enabled));
|
|
97
|
+
} else if (event.data?.kind === "cfp:overlay") {
|
|
98
|
+
const layer = typeof event.data.layer === "string" ? event.data.layer : "default";
|
|
99
|
+
setPageOverlay(layer, event.data.overlay as PageOverlayPayload | undefined);
|
|
100
|
+
} else if (event.data?.kind === "cfp:runtime-detail") {
|
|
101
|
+
const requestId = String(event.data.requestId ?? "");
|
|
102
|
+
const runtimeNodeId = String(event.data.runtimeNodeId ?? "");
|
|
103
|
+
const detail = collectRuntimeNodeDetail(runtimeNodeId);
|
|
104
|
+
publishToHost({ kind: "cfp-runtime-detail", requestId, detail });
|
|
105
|
+
} else if (event.data?.kind === "cfp:computed-style") {
|
|
106
|
+
// Computed Styles 视图的订阅:只在该视图可见且选中节点时建立,
|
|
107
|
+
// 之后由 MutationObserver + resize 触发增量推送(见 subscribeComputedStyle)。
|
|
108
|
+
const runtimeNodeId = String(event.data.runtimeNodeId ?? "");
|
|
109
|
+
if (event.data.subscribe === true && runtimeNodeId) {
|
|
110
|
+
subscribeComputedStyle(runtimeNodeId);
|
|
111
|
+
} else {
|
|
112
|
+
unsubscribeComputedStyle();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
document.addEventListener("click", handleInspectClick, true);
|
|
117
|
+
document.addEventListener("mousemove", handleInspectMove, true);
|
|
118
|
+
document.addEventListener("wheel", blockInspectedInteraction, { capture: true, passive: false });
|
|
119
|
+
document.addEventListener("keydown", handleInspectKeyDown, true);
|
|
120
|
+
for (const type of BLOCKED_INPUT_EVENTS) {
|
|
121
|
+
document.addEventListener(type, blockInspectedInteraction, true);
|
|
122
|
+
}
|
|
123
|
+
// Fallback for paths where the renderer's snapshot hook is not reached.
|
|
124
|
+
setTimeout(publishRuntimeSnapshot, 1000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function blockInspectedInteraction(event: Event): void {
|
|
128
|
+
if (!inspectEnabled) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
event.stopPropagation();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleInspectKeyDown(event: KeyboardEvent): void {
|
|
136
|
+
if (!inspectEnabled || !SCROLL_KEYS.has(event.key)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setInspectEnabled(enabled: boolean): void {
|
|
144
|
+
inspectEnabled = enabled;
|
|
145
|
+
// 两种渲染面共用这套 bridge:inspect 期间页面本身不可交互,输入只能留在覆盖层上。
|
|
146
|
+
setPageInteractionShield(enabled);
|
|
147
|
+
if (!enabled) {
|
|
148
|
+
clearPageOverlay("inspect");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// 键盘焦点可能还留在应用里(tab 聚焦的输入框):inspect 时不应该继续往里输入。
|
|
152
|
+
const active = document.activeElement;
|
|
153
|
+
if (active instanceof HTMLElement && active !== document.body && !isOverlayChrome(active)) {
|
|
154
|
+
active.blur();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleInspectClick(event: MouseEvent): void {
|
|
159
|
+
if (!inspectEnabled) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const element = elementUnderPointer(event.clientX, event.clientY);
|
|
163
|
+
const source = element ? getElementSource(element) : undefined;
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
publishToHost({ kind: "cfp-runtime-select", source });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleInspectMove(event: MouseEvent): void {
|
|
170
|
+
if (!inspectEnabled) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// 盾牌只是改了命中目标,事件依旧会从 document 冒泡给应用,先截住。
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
const element = elementUnderPointer(event.clientX, event.clientY);
|
|
176
|
+
if (!element) {
|
|
177
|
+
clearPageOverlay("inspect");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const rect = element.getBoundingClientRect();
|
|
181
|
+
const fiber = getElementFiberUpward(element);
|
|
182
|
+
const tag = element.tagName.toLowerCase();
|
|
183
|
+
const componentName = getComponentName(fiber);
|
|
184
|
+
const label = componentName ? `${componentName} <${tag}>` : `<${tag}>`;
|
|
185
|
+
setPageOverlay("inspect", {
|
|
186
|
+
visible: true,
|
|
187
|
+
items: [
|
|
188
|
+
{
|
|
189
|
+
x: rect.left,
|
|
190
|
+
y: rect.top,
|
|
191
|
+
width: rect.width,
|
|
192
|
+
height: rect.height,
|
|
193
|
+
label,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getElementSource(element: Element): RuntimeSourceLocation | undefined {
|
|
200
|
+
return getSourceLocation(getElementFiberUpward(element));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* inspect 的盾牌接管了命中测试,`elementFromPoint` 会返回覆盖层自己;
|
|
205
|
+
* 用 `elementsFromPoint` 的栈跳过覆盖层元素,才能拿到真正的应用节点。
|
|
206
|
+
*/
|
|
207
|
+
function elementUnderPointer(clientX: number, clientY: number): Element | undefined {
|
|
208
|
+
const stack = document.elementsFromPoint?.(clientX, clientY) ?? [];
|
|
209
|
+
const target = stack.find((element) => !isOverlayChrome(element));
|
|
210
|
+
if (target) {
|
|
211
|
+
return target;
|
|
212
|
+
}
|
|
213
|
+
const fallback = document.elementFromPoint(clientX, clientY);
|
|
214
|
+
return fallback && !isOverlayChrome(fallback) ? fallback : undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function publishRuntimeSnapshot(): void {
|
|
218
|
+
if (typeof window === "undefined") {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const snapshot = collectRuntimeSnapshot();
|
|
222
|
+
publishToHost({ kind: "cfp-runtime-snapshot", snapshot });
|
|
223
|
+
// 重新渲染会重建 nodeElements:订阅中的节点要么拿到新值,要么显式告知已消失。
|
|
224
|
+
if (computedStyleSubscription) {
|
|
225
|
+
scheduleComputedStylePublish();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Browser mode uses a CDP Runtime binding (`window.__cfpBridge`) because the
|
|
231
|
+
* preview page is a top-level page with no parent frame. Embedded mode keeps
|
|
232
|
+
* the ancestor postMessage path.
|
|
233
|
+
*/
|
|
234
|
+
function publishToHost(message: unknown): void {
|
|
235
|
+
if (typeof window.__cfpBridge === "function") {
|
|
236
|
+
try {
|
|
237
|
+
window.__cfpBridge(JSON.stringify(message));
|
|
238
|
+
return;
|
|
239
|
+
} catch {
|
|
240
|
+
// Fall through to postMessage if the binding context is gone.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!window.parent || window.parent === window) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
postToAncestors(message);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function postToAncestors(message: unknown): number {
|
|
250
|
+
const seen = new Set<Window>();
|
|
251
|
+
let posted = 0;
|
|
252
|
+
let target: any = window;
|
|
253
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
254
|
+
try {
|
|
255
|
+
const parent = target.parent;
|
|
256
|
+
if (!parent || parent === target || seen.has(parent)) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
seen.add(parent);
|
|
260
|
+
try {
|
|
261
|
+
parent.postMessage(message, "*");
|
|
262
|
+
} catch {
|
|
263
|
+
// Some embedders only preserve string messages; try the JSON fallback.
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
parent.postMessage(JSON.stringify(message), "*");
|
|
267
|
+
} catch {
|
|
268
|
+
// Ignore secondary transport failure.
|
|
269
|
+
}
|
|
270
|
+
posted += 1;
|
|
271
|
+
target = parent;
|
|
272
|
+
} catch {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return posted;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function collectRuntimeSnapshot(): RuntimeSnapshot {
|
|
280
|
+
const nodes: RuntimeNode[] = [];
|
|
281
|
+
nodeElements.clear();
|
|
282
|
+
const root = document.getElementById("root") ?? document.body;
|
|
283
|
+
const truncated = walk(root, undefined, "root", nodes);
|
|
284
|
+
return {
|
|
285
|
+
provider: "preview",
|
|
286
|
+
url: window.location.href,
|
|
287
|
+
capturedAt: Date.now(),
|
|
288
|
+
nodes,
|
|
289
|
+
diagnostics: truncated ? [`Node limit reached (${MAX_NODES})`] : undefined,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function walk(element: Element | null, parentId: string | undefined, path: string, nodes: RuntimeNode[]): boolean {
|
|
294
|
+
if (!element || nodes.length >= MAX_NODES) {
|
|
295
|
+
return nodes.length >= MAX_NODES;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const fiber = getElementFiberUpward(element);
|
|
299
|
+
const source = getSourceLocation(fiber);
|
|
300
|
+
// Path-based ID: stable across snapshots as long as the DOM structure is
|
|
301
|
+
// unchanged. The previous `dom:<index>` changed whenever a sibling appeared.
|
|
302
|
+
const id = `dom:${path}`;
|
|
303
|
+
const tag = element.tagName.toLowerCase();
|
|
304
|
+
const node: RuntimeNode = {
|
|
305
|
+
id,
|
|
306
|
+
parentId,
|
|
307
|
+
kind: "host",
|
|
308
|
+
name: tag,
|
|
309
|
+
componentName: getComponentName(fiber),
|
|
310
|
+
tag,
|
|
311
|
+
source,
|
|
312
|
+
children: [],
|
|
313
|
+
};
|
|
314
|
+
nodes.push(node);
|
|
315
|
+
nodeElements.set(id, element);
|
|
316
|
+
|
|
317
|
+
let truncated = false;
|
|
318
|
+
const children = Array.from(element.children);
|
|
319
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
320
|
+
if (truncated) {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
const childPath = `${path}/${index}`;
|
|
324
|
+
const childId = nodes.length < MAX_NODES ? `dom:${childPath}` : undefined;
|
|
325
|
+
if (!childId) {
|
|
326
|
+
truncated = true;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
node.children.push(childId);
|
|
330
|
+
truncated = walk(children[index]!, id, childPath, nodes) || truncated;
|
|
331
|
+
}
|
|
332
|
+
return truncated;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function collectRuntimeNodeDetail(runtimeNodeId: string): RuntimeNodeDetail | undefined {
|
|
336
|
+
const element = nodeElements.get(runtimeNodeId);
|
|
337
|
+
if (!element) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
const rect = element.getBoundingClientRect();
|
|
341
|
+
const fiber = getElementFiberUpward(element);
|
|
342
|
+
const attributes: Record<string, string> = {};
|
|
343
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
344
|
+
attributes[attribute.name] = attribute.value.slice(0, 500);
|
|
345
|
+
}
|
|
346
|
+
const text = (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
347
|
+
const outerHTML = element.outerHTML;
|
|
348
|
+
return {
|
|
349
|
+
runtimeNodeId,
|
|
350
|
+
bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
351
|
+
viewport: {
|
|
352
|
+
width: window.innerWidth,
|
|
353
|
+
height: window.innerHeight,
|
|
354
|
+
scrollX: window.scrollX,
|
|
355
|
+
scrollY: window.scrollY,
|
|
356
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
357
|
+
},
|
|
358
|
+
computedStyle: summarizeComputedStyle(window.getComputedStyle(element)),
|
|
359
|
+
dom: {
|
|
360
|
+
tag: element.tagName.toLowerCase(),
|
|
361
|
+
id: element.id || undefined,
|
|
362
|
+
className: element.getAttribute("class") ?? undefined,
|
|
363
|
+
attributes,
|
|
364
|
+
text: text ? text.slice(0, 500) : undefined,
|
|
365
|
+
childElementCount: element.childElementCount,
|
|
366
|
+
outerHTML: outerHTML.length > 2000 ? outerHTML.slice(0, 2000) : outerHTML,
|
|
367
|
+
outerHTMLTruncated: outerHTML.length > 2000,
|
|
368
|
+
},
|
|
369
|
+
// Render chain (who parented it) and owner chain (who authored the JSX) are
|
|
370
|
+
// different walks over the same fiber; see @cfp-studio/fiber-reader.
|
|
371
|
+
componentStack: collectFiberChain(genFiberRenderChain(fiber)),
|
|
372
|
+
ownerChain: collectFiberChain(genFiberSourceChain(fiber)),
|
|
373
|
+
capturedAt: Date.now(),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Computed Styles 视图的订阅态。
|
|
379
|
+
*
|
|
380
|
+
* 为什么不是"每次请求返回一次":视图要跟着 DOM / 样式变化刷新(Codux 的
|
|
381
|
+
* computed-styles 也是订阅式)。这里用 MutationObserver + resize,节流成
|
|
382
|
+
* "120ms 静默后推一次、最长 600ms 必推",并且只在摘要真的变了才发(避免噪声)。
|
|
383
|
+
*/
|
|
384
|
+
interface ComputedStyleSubscription {
|
|
385
|
+
runtimeNodeId: string;
|
|
386
|
+
/** 上次推送的摘要(JSON),用于去重。 */
|
|
387
|
+
lastPayload: string;
|
|
388
|
+
timer: number | undefined;
|
|
389
|
+
firstScheduledAt: number | undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const COMPUTED_STYLE_DEBOUNCE_MS = 120;
|
|
393
|
+
const COMPUTED_STYLE_MAX_DELAY_MS = 600;
|
|
394
|
+
|
|
395
|
+
let computedStyleSubscription: ComputedStyleSubscription | undefined;
|
|
396
|
+
let computedStyleObserver: MutationObserver | undefined;
|
|
397
|
+
let computedStyleResizeListener = false;
|
|
398
|
+
|
|
399
|
+
function publishComputedStyle(subscription: ComputedStyleSubscription): void {
|
|
400
|
+
const element = nodeElements.get(subscription.runtimeNodeId);
|
|
401
|
+
const computedStyle = element ? summarizeComputedStyle(window.getComputedStyle(element)) : null;
|
|
402
|
+
const payload = JSON.stringify(computedStyle);
|
|
403
|
+
if (payload === subscription.lastPayload) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
subscription.lastPayload = payload;
|
|
407
|
+
publishToHost({
|
|
408
|
+
kind: "cfp-computed-style",
|
|
409
|
+
runtimeNodeId: subscription.runtimeNodeId,
|
|
410
|
+
computedStyle,
|
|
411
|
+
capturedAt: Date.now(),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function scheduleComputedStylePublish(): void {
|
|
416
|
+
const subscription = computedStyleSubscription;
|
|
417
|
+
if (!subscription) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const now = Date.now();
|
|
421
|
+
subscription.firstScheduledAt ??= now;
|
|
422
|
+
if (subscription.timer !== undefined) {
|
|
423
|
+
window.clearTimeout(subscription.timer);
|
|
424
|
+
}
|
|
425
|
+
const remaining = COMPUTED_STYLE_MAX_DELAY_MS - (now - subscription.firstScheduledAt);
|
|
426
|
+
const delay = Math.max(0, Math.min(COMPUTED_STYLE_DEBOUNCE_MS, remaining));
|
|
427
|
+
subscription.timer = window.setTimeout(() => {
|
|
428
|
+
if (computedStyleSubscription !== subscription) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
subscription.timer = undefined;
|
|
432
|
+
subscription.firstScheduledAt = undefined;
|
|
433
|
+
publishComputedStyle(subscription);
|
|
434
|
+
}, delay);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isOverlayMutation(record: MutationRecord): boolean {
|
|
438
|
+
const target = record.target;
|
|
439
|
+
if (target instanceof Element) {
|
|
440
|
+
return isOverlayChrome(target);
|
|
441
|
+
}
|
|
442
|
+
const parent = target.parentElement;
|
|
443
|
+
return Boolean(parent && isOverlayChrome(parent));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function subscribeComputedStyle(runtimeNodeId: string): void {
|
|
447
|
+
if (computedStyleSubscription?.runtimeNodeId === runtimeNodeId) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
unsubscribeComputedStyle();
|
|
451
|
+
const subscription: ComputedStyleSubscription = {
|
|
452
|
+
runtimeNodeId,
|
|
453
|
+
lastPayload: "",
|
|
454
|
+
timer: undefined,
|
|
455
|
+
firstScheduledAt: undefined,
|
|
456
|
+
};
|
|
457
|
+
computedStyleSubscription = subscription;
|
|
458
|
+
if (!computedStyleObserver && typeof MutationObserver !== "undefined") {
|
|
459
|
+
computedStyleObserver = new MutationObserver((records) => {
|
|
460
|
+
// inspect 悬停会让 overlay 自己变形;shadow root 内的变更本来就不上报,
|
|
461
|
+
// 但 host 节点的属性变更会——这类变更不影响被选节点的 computed style。
|
|
462
|
+
if (records.every(isOverlayMutation)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
scheduleComputedStylePublish();
|
|
466
|
+
});
|
|
467
|
+
// 观察整棵树(父/兄弟节点变化也会改 computed style);overlay 自身的变更
|
|
468
|
+
// 由 isOverlayChrome 过滤,避免 inspect 悬停时无谓重算。
|
|
469
|
+
computedStyleObserver.observe(document.documentElement, {
|
|
470
|
+
subtree: true,
|
|
471
|
+
childList: true,
|
|
472
|
+
attributes: true,
|
|
473
|
+
characterData: true,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!computedStyleResizeListener) {
|
|
477
|
+
window.addEventListener("resize", scheduleComputedStylePublish);
|
|
478
|
+
computedStyleResizeListener = true;
|
|
479
|
+
}
|
|
480
|
+
publishComputedStyle(subscription);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function unsubscribeComputedStyle(): void {
|
|
484
|
+
const subscription = computedStyleSubscription;
|
|
485
|
+
if (subscription?.timer !== undefined) {
|
|
486
|
+
window.clearTimeout(subscription.timer);
|
|
487
|
+
}
|
|
488
|
+
computedStyleSubscription = undefined;
|
|
489
|
+
computedStyleObserver?.disconnect();
|
|
490
|
+
computedStyleObserver = undefined;
|
|
491
|
+
if (computedStyleResizeListener) {
|
|
492
|
+
window.removeEventListener("resize", scheduleComputedStylePublish);
|
|
493
|
+
computedStyleResizeListener = false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Turn a fiber chain into packet frames, root → node.
|
|
499
|
+
*
|
|
500
|
+
* Host / text / fragment / portal frames are dropped: the DOM ancestor chain is
|
|
501
|
+
* `domPath`'s job, and mixing the two is exactly what schema v2 separated.
|
|
502
|
+
* Layers that have no name of their own (`memo`, `Suspense`, `Provider`, …) keep
|
|
503
|
+
* their place via `kind` instead of silently vanishing.
|
|
504
|
+
*/
|
|
505
|
+
function collectFiberChain(fibers: Iterable<FiberLike>): RuntimeComponentFrame[] {
|
|
506
|
+
const frames: RuntimeComponentFrame[] = [];
|
|
507
|
+
for (const fiber of fibers) {
|
|
508
|
+
const kind = resolveFiberTagKind(fiber);
|
|
509
|
+
if (!kind || kind === "host" || kind === "text" || kind === "fragment" || kind === "portal") {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const name = getDisplayNameForFiber(fiber);
|
|
513
|
+
if (!name) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const previous = frames[frames.length - 1];
|
|
517
|
+
// Consecutive duplicates carry no information (same component, same source).
|
|
518
|
+
if (previous && previous.name === name && previous.kind === kind) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const source = getSourceLocation(fiber);
|
|
522
|
+
frames.push(source ? { name, kind, source } : { name, kind });
|
|
523
|
+
}
|
|
524
|
+
return frames.reverse();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Source position plus which tier it came from. */
|
|
528
|
+
function getSourceLocation(fiber: FiberLike | undefined): RuntimeSourceLocation | undefined {
|
|
529
|
+
const location = getFiberSourceLocation(fiber);
|
|
530
|
+
if (!location) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
...location,
|
|
535
|
+
evidence: getCodeInfoFromDebugSource(fiber)
|
|
536
|
+
? "_debugSource"
|
|
537
|
+
: getCodeInfoFromProps(fiber)
|
|
538
|
+
? "data-inspector"
|
|
539
|
+
: "owner",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function getComponentName(fiber: FiberLike | undefined): string | undefined {
|
|
544
|
+
let current = fiber;
|
|
545
|
+
// `getDisplayNameForFiber` resolves memo/forwardRef/provider layers; fall back
|
|
546
|
+
// to the nearest frame that actually has a name.
|
|
547
|
+
while (current) {
|
|
548
|
+
const name = getDisplayNameForFiber(current);
|
|
549
|
+
if (name && !isNativeTag(current)) {
|
|
550
|
+
return name;
|
|
551
|
+
}
|
|
552
|
+
current = current.return ?? undefined;
|
|
553
|
+
}
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isNativeTag(fiber: FiberLike): boolean {
|
|
558
|
+
return typeof fiber.type === "string";
|
|
559
|
+
}
|
|
@@ -1,58 +1,83 @@
|
|
|
1
1
|
export function setupVSCodeControlListener() {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
window.addEventListener("message", (event) => {
|
|
3
|
+
if (typeof event.data !== "string") {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
parseMessageFromChannel(event.data, (eventName, args) => {
|
|
7
|
+
console.debug(`收到vscode:${eventName}消息: ${args}`);
|
|
8
|
+
if (eventName === "ready") {
|
|
9
|
+
const { message } = JSON.parse(args) as { message: string };
|
|
10
|
+
const payload = JSON.parse(message);
|
|
11
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
12
|
+
kind: "toggle-inspect",
|
|
13
|
+
enabled: payload.data.isInspectorOpen,
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function parseMessageFromChannel(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
message: string,
|
|
24
|
+
emit: (eventName: WebviewEvent, args: string) => boolean,
|
|
25
|
+
): boolean {
|
|
26
|
+
// 预览页会收到两类消息:vscode 频道字符串,以及 cfp-* 对象(runtime-bridge / inspect)。
|
|
27
|
+
// 这里必须自己挡一次——只要调用方漏判,非字符串就会走到 substr 上炸掉整个监听器。
|
|
28
|
+
if (typeof message !== "string") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
for (const e of webviewEventNames) {
|
|
32
|
+
if (message.substr(0, e.length) === e && message[e.length] === ":") {
|
|
33
|
+
emit(e, message.substr(e.length + 1));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
return false;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
export type WebviewEvent =
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
export type WebviewEvent =
|
|
42
|
+
| "getState"
|
|
43
|
+
| "getUrl"
|
|
44
|
+
| "openInEditor"
|
|
45
|
+
| "cssMirrorContent"
|
|
46
|
+
| "ready"
|
|
47
|
+
| "setState"
|
|
48
|
+
| "telemetry"
|
|
49
|
+
| "websocket"
|
|
50
|
+
| "getVscodeSettings"
|
|
51
|
+
| "copyText"
|
|
52
|
+
| "focusEditor"
|
|
53
|
+
| "focusEditorGroup"
|
|
54
|
+
| "openUrl"
|
|
55
|
+
| "toggleScreencast"
|
|
56
|
+
| "toggleInspect"
|
|
57
|
+
| "replayConsoleMessages"
|
|
58
|
+
| "devtoolsConnection"
|
|
59
|
+
| "toggleCSSMirrorContent"
|
|
60
|
+
| "writeToClipboard"
|
|
61
|
+
| "readClipboard";
|
|
37
62
|
export const webviewEventNames: WebviewEvent[] = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
];
|
|
63
|
+
"getState",
|
|
64
|
+
"getUrl",
|
|
65
|
+
"openInEditor",
|
|
66
|
+
"cssMirrorContent",
|
|
67
|
+
"ready",
|
|
68
|
+
"setState",
|
|
69
|
+
"telemetry",
|
|
70
|
+
"websocket",
|
|
71
|
+
"getVscodeSettings",
|
|
72
|
+
"copyText",
|
|
73
|
+
"focusEditor",
|
|
74
|
+
"focusEditorGroup",
|
|
75
|
+
"openUrl",
|
|
76
|
+
"toggleScreencast",
|
|
77
|
+
"toggleInspect",
|
|
78
|
+
"replayConsoleMessages",
|
|
79
|
+
"devtoolsConnection",
|
|
80
|
+
"toggleCSSMirrorContent",
|
|
81
|
+
"writeToClipboard",
|
|
82
|
+
"readClipboard",
|
|
83
|
+
];
|