@cfp-studio/iframe 18.4.3 → 18.4.5

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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../../../node_modules/.pnpm/vite@6.4.5_@types+node@24.19.2_jiti@2.7.0_less@4.2.2_lightningcss@1.30.2_sass@1.85.1_terser@5.40.0_tsx@4.19.3_yaml@2.7.0/node_modules/vite/types/hmrPayload.d.ts
1
+ //#region ../../../../node_modules/.pnpm/vite@6.4.5_@types+node@24.1_13c54df9746541450cb8a36e10ae9cdd/node_modules/vite/types/hmrPayload.d.ts
2
2
  interface UpdatePayload {
3
3
  type: 'update';
4
4
  updates: Update[];
@@ -33,6 +33,14 @@ declare global {
33
33
  */
34
34
  lastRenderFailed: boolean;
35
35
  toggleInspect: boolean;
36
+ /**
37
+ * 编译期错误兜底开关(`index.html` 的内联脚本用)。
38
+ *
39
+ * `true` 时由 `index.html` 上报那些「模块还没加载就崩」的错误
40
+ * (Vite 解析 `.less` 失败、`react-dom` 缺失等);`logs.ts` 接管后置 `false`。
41
+ * 见 `docs/todo/39`。
42
+ */
43
+ startupErrorGuard: boolean;
36
44
  /**
37
45
  * 处理 HMR 更新
38
46
  * @param payload - HMR 更新的负载
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfp-studio/iframe",
3
- "version": "18.4.3",
3
+ "version": "18.4.5",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist",
@@ -10,6 +10,7 @@
10
10
  "main": "./dist/index.mjs",
11
11
  "types": "./dist/index.d.mts",
12
12
  "dependencies": {
13
+ "@cfp-studio/analyzer-api": "0.1.15",
13
14
  "@cfp-studio/fiber-reader": "0.3.0"
14
15
  },
15
16
  "devDependencies": {
@@ -1,3 +1,5 @@
1
+ import { clearErrorPanel } from "@cfp-studio/analyzer-api";
2
+
1
3
  import { overrideCopyCutPaste } from "./copy-cut-paste";
2
4
  import { setUpLinkInterception } from "./links";
3
5
  import { setUpLogInterception } from "./logs";
@@ -67,11 +69,20 @@ export function initPreview({
67
69
  shouldAbortRender: () => renderId !== thisRenderId,
68
70
  loadRenderer,
69
71
  });
72
+ // 渲染成功:上一次的错误覆盖层该撤了。
73
+ //
74
+ // 面板现在挂在 `document.body` 的独立覆盖层上(不占 `#root`),所以不会被
75
+ // `root.innerHTML` 覆写带走 —— 必须显式清 —— 否则改好代码后面板会永久赖着。
76
+ clearErrorPanel();
70
77
  } catch (error: any) {
71
- if (root.innerHTML !== rootHtml) {
72
- // 重新调用前一次渲染,以确保预览不会白屏。
73
- root.innerHTML = rootHtml;
74
- }
78
+ // **不再 `root.innerHTML = rootHtml` 回填快照**。
79
+ //
80
+ // 旧行为(为了「不白屏」而回滚)实测症状:「错误面板闪现一下,然后被正常页面
81
+ // 覆盖」,用户看不到真正原因。现在改成:保留错误覆盖层(半透明遮罩盖住旧画面),
82
+ // 同时继续 `reportEvent` 给宿主侧。
83
+ //
84
+ // `rootHtml` 仍用于判定:没变化说明连上一次的 DOM 都没建立,无需特别处理。
85
+ void rootHtml;
75
86
  window.__PREVIEWJS_IFRAME__.reportEvent({
76
87
  kind: "error",
77
88
  source: "renderer",
@@ -45,6 +45,10 @@ export function setUpLogInterception() {
45
45
  console.info = makeLogger("info", console.info);
46
46
  console.warn = makeLogger("warn", console.warn);
47
47
  console.error = makeLogger("error", console.error);
48
+ // 接管 `index.html` 的编译期兜底(见 `docs/todo/39`):
49
+ // 本模块靠模块加载才能执行,所以更早的失败只能由 `index.html` 的内联脚本接住。
50
+ // 现在轮到我们了,关掉那个兜底,避免同一个错误上报两次。
51
+ window.__PREVIEWJS_IFRAME__.startupErrorGuard = false;
48
52
  window.onerror = (message, source, lineno, colno, error) => {
49
53
  window.__PREVIEWJS_IFRAME__.reportEvent({
50
54
  kind: "error",
@@ -642,11 +642,30 @@ function collectRuntimeSnapshot(): RuntimeSnapshot {
642
642
  };
643
643
  }
644
644
 
645
+ /**
646
+ * 渲染错误面板的标记属性(`docs/todo/39`)。
647
+ *
648
+ * 与 `@cfp-studio/analyzer-api` 的 `ERROR_PANEL_ATTRIBUTE` 保持一致。
649
+ * 面板是**预览工具自己画的内容**,不是被预览组件的一部分 ——
650
+ * 不排除的话它会作为普通 DOM 出现在 Runtime Tree 里,还会参与点击选框命中。
651
+ */
652
+ const ERROR_PANEL_ATTRIBUTE = "data-preview-error-panel";
653
+
654
+ /** 该元素是否是预览工具自己的错误面板。 */
655
+ function isErrorPanel(element: Element): boolean {
656
+ return element.hasAttribute?.(ERROR_PANEL_ATTRIBUTE) ?? false;
657
+ }
658
+
645
659
  function walk(element: Element | null, parentId: string | undefined, path: string, nodes: RuntimeNode[]): boolean {
646
660
  if (!element || nodes.length >= MAX_NODES) {
647
661
  return nodes.length >= MAX_NODES;
648
662
  }
649
663
 
664
+ if (isErrorPanel(element)) {
665
+ // 跳过面板自身与它的子树,但不计为截断。
666
+ return false;
667
+ }
668
+
650
669
  const fiber = getElementFiberUpward(element);
651
670
  const source = getSourceLocation(fiber);
652
671
  // Path-based ID: stable across snapshots as long as the DOM structure is
@@ -46,7 +46,15 @@ export function setupViteHmrListener() {
46
46
  isFirstUpdate = false;
47
47
  }
48
48
  if (window.__PREVIEWJS_IFRAME__.lastRenderFailed) {
49
- window.location.reload();
49
+ // **不要 `location.reload()`**。
50
+ //
51
+ // 旧行为:上一次渲染失败时整页重载。实测症状(真实反馈):
52
+ // 「保存后闪现错误页面,然后被白屏覆盖」—— 错误面板刚画出来就被重载抹了,
53
+ // 重载后 `runNewRender` 又在 `loadRenderer` 阶段抛错,用户什么都看不到。
54
+ //
55
+ // 现在:面板是独立覆盖层(见 `analyzer-api` 的 `renderErrorPanel`),
56
+ // 只要重新渲染就行 —— 成功时 `runNewRender` 会自己 `clearErrorPanel()`。
57
+ window.__PREVIEWJS_IFRAME__.refresh({ refetchPreviewableModule: true });
50
58
  } else {
51
59
  window.__PREVIEWJS_IFRAME__.reportEvent({
52
60
  kind: "vite-before-update",
@@ -13,6 +13,8 @@
13
13
  // 初始化 lastRenderFailed 为 false,表示初始渲染未失败
14
14
  lastRenderFailed: false,
15
15
  toggleInspect: false,
16
+ /** 编译期兜底上报开关:`logs.ts` 接管后置 false(见 `docs/todo/39`)。 */
17
+ startupErrorGuard: true,
16
18
  /**
17
19
  * 报告预览事件
18
20
  * @param {Object} event - 事件对象
@@ -56,6 +58,47 @@
56
58
  window.__PREVIEWJS_IFRAME__.reportEvent({
57
59
  kind: "bootstrapping",
58
60
  });
61
+
62
+ /**
63
+ * 编译期错误的兜底上报(`docs/todo/39`)。
64
+ *
65
+ * **为什么必须放在这里**:Vite 的模块图加载失败(例如 `.less` 缺 `less` 包、
66
+ * `react-dom` 未安装)发生在**任何业务 JS 执行之前** —— `__previewjs_internal__/logs.ts`
67
+ * 里那套 `window.onerror` 自己也靠模块加载才能注册,救不了更早的失败。
68
+ * 结果就是「白屏 + 零报错」,用户无从下手。
69
+ *
70
+ * 这里是内联脚本,一定先于模块执行,所以能接住那些“还没进入应用就崩”的错误。
71
+ * 模块加载成功后,`logs.ts` 会把这两个钩子**覆盖**成更完整的版本(带格式化与
72
+ * `vite:error` 分流),这属于预期叠加:本处只要求“不静默”。
73
+ */
74
+ const reportStartupError = (raw) => {
75
+ // `logs.ts` 加载成功后会把这个开关关掉(它有更完整的格式化与 vite:error 分流),
76
+ // 避免同一个错误被上报两次。
77
+ if (!window.__PREVIEWJS_IFRAME__.startupErrorGuard) {
78
+ return;
79
+ }
80
+ const text = raw instanceof Error ? raw.stack || raw.message : String(raw);
81
+ window.__PREVIEWJS_IFRAME__.reportEvent({
82
+ kind: "error",
83
+ source: "vite",
84
+ message: text,
85
+ });
86
+ };
87
+
88
+ window.addEventListener("error", (event) => {
89
+ // 只接模块脚本 / 资源加载失败,其它错误交给 logs.ts 的版本。
90
+ if (event.target && event.target !== window && event.target.tagName) {
91
+ reportStartupError(`Failed to load ${event.target.tagName.toLowerCase()}: ${event.target.src || event.target.href || ""}`);
92
+ return;
93
+ }
94
+ if (event.error || event.message) {
95
+ reportStartupError(event.error || event.message);
96
+ }
97
+ }, true);
98
+
99
+ window.addEventListener("unhandledrejection", (event) => {
100
+ reportStartupError(event.reason);
101
+ });
59
102
  </script>
60
103
  <!-- %OPTIONAL_HEAD_CONTENT% -->
61
104
  <style>