@cfp-studio/iframe 18.0.1 → 18.2.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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@cfp-studio/iframe",
3
- "version": "18.0.1",
3
+ "version": "18.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
7
+ "types": "./dist/index.d.mts",
8
8
  "files": [
9
9
  "dist",
10
10
  "preview"
@@ -12,12 +12,17 @@
12
12
  "devDependencies": {
13
13
  "assert-never": "1.4.0",
14
14
  "immer": "10.1.1",
15
- "typescript": "5.7.3",
16
- "unbuild": "2.0.0",
17
- "vite": "6.2.0"
15
+ "typescript": "5.9.3",
16
+ "vite": "6.4.5"
17
+ },
18
+ "dependencies": {
19
+ "@cfp-studio/fiber-reader": "0.1.0"
18
20
  },
19
21
  "scripts": {
20
- "build": "tsc && unbuild",
21
- "release": "tsc && unbuild --minify"
22
+ "build": "tsdown",
23
+ "typecheck": "tsc --noEmit",
24
+ "release": "tsdown --minify",
25
+ "lint": "oxlint",
26
+ "format": "oxfmt --write ."
22
27
  }
23
28
  }
@@ -4,6 +4,7 @@ import { setUpLogInterception } from "./logs";
4
4
  import { loadRenderer } from "./renderer";
5
5
  import { runRenderer } from "./run-renderer";
6
6
  import { setState } from "./state";
7
+ import { setupRuntimeBridge } from "./runtime-bridge";
7
8
  import { setupViteHmrListener } from "./vite-hmr-listener";
8
9
  import { setupVSCodeControlListener } from "./vscode-control";
9
10
 
@@ -21,6 +22,7 @@ export function initListeners() {
21
22
  setUpLinkInterception();
22
23
  overrideCopyCutPaste();
23
24
  setupVSCodeControlListener();
25
+ setupRuntimeBridge();
24
26
  }
25
27
 
26
28
  export function initPreview({
@@ -29,10 +31,8 @@ export function initPreview({
29
31
  wrapperModule,
30
32
  wrapperName,
31
33
  }: {
32
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
33
34
  previewableModule: any;
34
35
  previewableName: string;
35
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
36
36
  wrapperModule: any;
37
37
  wrapperName: string;
38
38
  }): typeof window.__PREVIEWJS_IFRAME__.refresh {
@@ -67,7 +67,6 @@ export function initPreview({
67
67
  shouldAbortRender: () => renderId !== thisRenderId,
68
68
  loadRenderer,
69
69
  });
70
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
71
70
  } catch (error: any) {
72
71
  if (root.innerHTML !== rootHtml) {
73
72
  // 重新调用前一次渲染,以确保预览不会白屏。
@@ -8,7 +8,6 @@ export function setUpLinkInterception() {
8
8
  break;
9
9
  }
10
10
  if (node.nodeName === "A") {
11
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
12
11
  const url = (node as any).href;
13
12
  // Handle click here by posting data back to VS Code
14
13
  // for your extension to handle
@@ -14,7 +14,6 @@ const HMR_FAILED_UPDATE_REGEX =
14
14
  export function setUpLogInterception() {
15
15
  const makeLogger =
16
16
  (level: LogLevel, defaultFn: typeof console.log) =>
17
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
18
17
  (...args: any[]) => {
19
18
  // Prevent an infinite call stack size if anything here logs.
20
19
  const self = console[level];
@@ -74,7 +73,6 @@ export function setUpLogInterception() {
74
73
  * @returns
75
74
  */
76
75
 
77
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
78
76
  function formatError(error?: any, message?: any): string {
79
77
  // 如果 error 对象存在且包含 stack 和 message 属性
80
78
  if (error?.stack && error.message) {
@@ -95,7 +93,6 @@ function formatError(error?: any, message?: any): string {
95
93
  * @param args
96
94
  * @returns
97
95
  */
98
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
99
96
  function formatLogMessage(...args: any[]) {
100
97
  if (args.length === 0) {
101
98
  return "";
@@ -110,7 +107,6 @@ function formatLogMessage(...args: any[]) {
110
107
  * @param value
111
108
  * @returns
112
109
  */
113
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
114
110
  function formatValue(value: any) {
115
111
  if (typeof value === "string") {
116
112
  return value;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Page-internal overlay channel.
3
+ *
4
+ * Overlays live inside the preview page so they remain aligned with the
5
+ * rendered pixels in external browser mode. A Shadow DOM host isolates styles
6
+ * from the inspected application. Each feature owns a named layer so clearing
7
+ * one overlay cannot clear another feature's overlay.
8
+ */
9
+
10
+ export interface PageOverlayItem {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ label?: string;
16
+ color?: string;
17
+ borderWidth?: number;
18
+ }
19
+
20
+ export interface PageOverlayPayload {
21
+ visible: boolean;
22
+ items: PageOverlayItem[];
23
+ }
24
+
25
+ let host: HTMLDivElement | undefined;
26
+ let shadowRoot: ShadowRoot | undefined;
27
+ const layers = new Map<string, HTMLDivElement>();
28
+
29
+ export function setPageOverlay(layerId: string, payload: PageOverlayPayload | undefined): void {
30
+ if (!payload?.visible || payload.items.length === 0) {
31
+ clearPageOverlay(layerId);
32
+ return;
33
+ }
34
+ ensureHost();
35
+ const layer = ensureLayer(layerId);
36
+ layer.replaceChildren();
37
+ for (const item of payload.items) {
38
+ const element = document.createElement("div");
39
+ element.className = "cfp-overlay-item";
40
+ element.style.left = `${item.x}px`;
41
+ element.style.top = `${item.y}px`;
42
+ element.style.width = `${item.width}px`;
43
+ element.style.height = `${item.height}px`;
44
+ element.style.borderColor = item.color ?? "#3794ff";
45
+ element.style.borderWidth = `${item.borderWidth ?? 1}px`;
46
+ if (item.label) {
47
+ const label = document.createElement("span");
48
+ label.className = "cfp-overlay-label";
49
+ label.textContent = item.label;
50
+ element.appendChild(label);
51
+ }
52
+ layer.appendChild(element);
53
+ }
54
+ }
55
+
56
+ export function clearPageOverlay(layerId?: string): void {
57
+ if (!layerId) {
58
+ host?.remove();
59
+ host = undefined;
60
+ shadowRoot = undefined;
61
+ layers.clear();
62
+ return;
63
+ }
64
+ layers.get(layerId)?.remove();
65
+ layers.delete(layerId);
66
+ if (layers.size === 0) {
67
+ host?.remove();
68
+ host = undefined;
69
+ shadowRoot = undefined;
70
+ }
71
+ }
72
+
73
+ let shieldHost: HTMLDivElement | undefined;
74
+
75
+ /**
76
+ * inspect 期间拦掉页面自身的指针交互:用户只能和覆盖层打交道。
77
+ *
78
+ * 用一个真实元素接管命中测试,而不是靠 `pointer-events: none` + preventDefault:
79
+ * 这样 CSS `:hover`、光标、文本选择、拖拽、滚动、表单聚焦都不会落到应用上。
80
+ * 代价是 `elementFromPoint` 会返回盾牌本身,所以命中测试要换成
81
+ * `elementsFromPoint` 并跳过覆盖层元素(见 `isOverlayChrome`)。
82
+ */
83
+ export function setPageInteractionShield(enabled: boolean): void {
84
+ if (typeof document === "undefined") {
85
+ return;
86
+ }
87
+ if (!enabled) {
88
+ shieldHost?.remove();
89
+ shieldHost = undefined;
90
+ return;
91
+ }
92
+ if (shieldHost?.isConnected) {
93
+ return;
94
+ }
95
+ const element = document.createElement("div");
96
+ element.dataset.cfpShield = "true";
97
+ element.setAttribute("aria-hidden", "true");
98
+ // 与应用样式隔离:`* { cursor: ... }` / `touch-action` 之类的规则不应影响盾牌。
99
+ const root = element.attachShadow({ mode: "closed" });
100
+ const style = document.createElement("style");
101
+ style.textContent = `
102
+ :host {
103
+ all: initial;
104
+ position: fixed;
105
+ inset: 0;
106
+ z-index: 2147483646;
107
+ cursor: crosshair;
108
+ touch-action: none;
109
+ overscroll-behavior: contain;
110
+ }
111
+ div {
112
+ position: fixed;
113
+ inset: 0;
114
+ background: transparent;
115
+ }
116
+ `;
117
+ root.append(style, document.createElement("div"));
118
+ document.documentElement.appendChild(element);
119
+ shieldHost = element;
120
+ }
121
+
122
+ /** 覆盖层自身的 DOM(overlay host / 盾牌);命中测试必须跳过它们。 */
123
+ export function isOverlayChrome(element: Element): boolean {
124
+ return Boolean(element.closest("[data-cfp-overlay], [data-cfp-shield]"));
125
+ }
126
+
127
+ function ensureLayer(layerId: string): HTMLDivElement {
128
+ const existing = layers.get(layerId);
129
+ if (existing?.isConnected) {
130
+ return existing;
131
+ }
132
+ if (!shadowRoot) {
133
+ throw new Error("Overlay shadow root is not available");
134
+ }
135
+ const layer = document.createElement("div");
136
+ layer.className = "cfp-overlay-layer";
137
+ layer.dataset.cfpOverlayLayer = layerId;
138
+ shadowRoot.appendChild(layer);
139
+ layers.set(layerId, layer);
140
+ return layer;
141
+ }
142
+
143
+ function ensureHost(): void {
144
+ if (host?.isConnected && shadowRoot) {
145
+ return;
146
+ }
147
+ host = document.createElement("div");
148
+ host.dataset.cfpOverlay = "true";
149
+ host.setAttribute("aria-hidden", "true");
150
+ shadowRoot = host.attachShadow({ mode: "closed" });
151
+ const style = document.createElement("style");
152
+ style.textContent = `
153
+ :host {
154
+ all: initial;
155
+ position: fixed;
156
+ inset: 0;
157
+ z-index: 2147483647;
158
+ pointer-events: none;
159
+ contain: layout style;
160
+ }
161
+ .cfp-overlay-layer {
162
+ position: fixed;
163
+ inset: 0;
164
+ pointer-events: none;
165
+ }
166
+ .cfp-overlay-item {
167
+ position: fixed;
168
+ box-sizing: border-box;
169
+ pointer-events: none;
170
+ border-style: solid;
171
+ background: rgba(55, 148, 255, 0.12);
172
+ }
173
+ .cfp-overlay-label {
174
+ position: absolute;
175
+ left: 0;
176
+ top: -20px;
177
+ padding: 2px 5px;
178
+ border-radius: 3px;
179
+ background: #3794ff;
180
+ color: white;
181
+ font: 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
182
+ white-space: nowrap;
183
+ }
184
+ `;
185
+ shadowRoot.appendChild(style);
186
+ document.documentElement.appendChild(host);
187
+ }
@@ -1,4 +1,5 @@
1
1
  import type { RendererLoader } from "../../src";
2
+ import { publishRuntimeSnapshot } from "./runtime-bridge";
2
3
  import { getState } from "./state";
3
4
 
4
5
  export async function runRenderer({
@@ -10,10 +11,8 @@ export async function runRenderer({
10
11
  shouldAbortRender,
11
12
  loadRenderer,
12
13
  }: {
13
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
14
14
  wrapperModule: any;
15
15
  wrapperName: string;
16
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
17
16
  previewableModule: any;
18
17
  previewableName: string;
19
18
  renderId: number;
@@ -65,6 +64,9 @@ export async function runRenderer({
65
64
  if (shouldAbortRender()) {
66
65
  return;
67
66
  }
67
+ // Publish before reportEvent: custom preview webviews may not install a
68
+ // __PREVIEWJS_CONTROLLER__, while the runtime bridge is independent.
69
+ setTimeout(publishRuntimeSnapshot, 0);
68
70
  window.__PREVIEWJS_IFRAME__.reportEvent({
69
71
  kind: "rendered",
70
72
  });
@@ -75,7 +77,6 @@ export async function runRenderer({
75
77
  * and shown to the user.
76
78
  */
77
79
 
78
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
79
80
  function transformFunctions(value: any, path: string[]): any {
80
81
  if (value && typeof value === "object") {
81
82
  if (Array.isArray(value)) {