@vobs/runtime 1.7.8 → 1.8.1

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.
Files changed (127) hide show
  1. package/README.md +1 -1
  2. package/dist/async-boundary.cjs +14 -4
  3. package/dist/async-boundary.cjs.map +1 -1
  4. package/dist/async-boundary.js +17 -7
  5. package/dist/async-boundary.js.map +1 -1
  6. package/dist/bind.cjs +43 -3
  7. package/dist/bind.cjs.map +1 -1
  8. package/dist/bind.js +48 -8
  9. package/dist/bind.js.map +1 -1
  10. package/dist/boundary.cjs +159 -84
  11. package/dist/boundary.cjs.map +1 -1
  12. package/dist/boundary.js +162 -87
  13. package/dist/boundary.js.map +1 -1
  14. package/dist/client-only.cjs +323 -0
  15. package/dist/client-only.cjs.map +1 -0
  16. package/dist/client-only.d.cts +42 -0
  17. package/dist/client-only.d.ts +42 -0
  18. package/dist/client-only.js +299 -0
  19. package/dist/client-only.js.map +1 -0
  20. package/dist/debug.cjs.map +1 -1
  21. package/dist/debug.d.cts +12 -2
  22. package/dist/debug.d.ts +12 -2
  23. package/dist/debug.js.map +1 -1
  24. package/dist/dom-events.cjs +46 -0
  25. package/dist/dom-events.cjs.map +1 -0
  26. package/dist/dom-events.d.cts +23 -0
  27. package/dist/dom-events.d.ts +23 -0
  28. package/dist/dom-events.js +21 -0
  29. package/dist/dom-events.js.map +1 -0
  30. package/dist/dom-props.cjs +154 -0
  31. package/dist/dom-props.cjs.map +1 -0
  32. package/dist/dom-props.d.cts +21 -0
  33. package/dist/dom-props.d.ts +21 -0
  34. package/dist/dom-props.js +128 -0
  35. package/dist/dom-props.js.map +1 -0
  36. package/dist/dynamic.cjs +68 -14
  37. package/dist/dynamic.cjs.map +1 -1
  38. package/dist/dynamic.js +73 -19
  39. package/dist/dynamic.js.map +1 -1
  40. package/dist/error-boundary.cjs +159 -84
  41. package/dist/error-boundary.cjs.map +1 -1
  42. package/dist/error-boundary.js +162 -87
  43. package/dist/error-boundary.js.map +1 -1
  44. package/dist/error.cjs +52 -20
  45. package/dist/error.cjs.map +1 -1
  46. package/dist/error.d.cts +8 -0
  47. package/dist/error.d.ts +8 -0
  48. package/dist/error.js +52 -20
  49. package/dist/error.js.map +1 -1
  50. package/dist/fragment.cjs.map +1 -1
  51. package/dist/fragment.js +1 -1
  52. package/dist/fragment.js.map +1 -1
  53. package/dist/hmr.cjs +137 -1
  54. package/dist/hmr.cjs.map +1 -1
  55. package/dist/hmr.js +135 -1
  56. package/dist/hmr.js.map +1 -1
  57. package/dist/index.cjs +713 -236
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +5 -2
  60. package/dist/index.d.ts +5 -2
  61. package/dist/index.js +715 -245
  62. package/dist/index.js.map +1 -1
  63. package/dist/ops.cjs +561 -107
  64. package/dist/ops.cjs.map +1 -1
  65. package/dist/ops.d.cts +49 -4
  66. package/dist/ops.d.ts +49 -4
  67. package/dist/ops.js +557 -108
  68. package/dist/ops.js.map +1 -1
  69. package/dist/profiler.cjs +14 -4
  70. package/dist/profiler.cjs.map +1 -1
  71. package/dist/profiler.js +17 -7
  72. package/dist/profiler.js.map +1 -1
  73. package/dist/ref.cjs +160 -5
  74. package/dist/ref.cjs.map +1 -1
  75. package/dist/ref.js +160 -5
  76. package/dist/ref.js.map +1 -1
  77. package/dist/renderer.cjs.map +1 -1
  78. package/dist/renderer.d.cts +2 -0
  79. package/dist/renderer.d.ts +2 -0
  80. package/dist/show.cjs +196 -0
  81. package/dist/show.cjs.map +1 -0
  82. package/dist/show.d.cts +54 -0
  83. package/dist/show.d.ts +54 -0
  84. package/dist/show.js +171 -0
  85. package/dist/show.js.map +1 -0
  86. package/dist/svg.cjs +102 -0
  87. package/dist/svg.cjs.map +1 -0
  88. package/dist/svg.d.cts +21 -0
  89. package/dist/svg.d.ts +21 -0
  90. package/dist/svg.js +76 -0
  91. package/dist/svg.js.map +1 -0
  92. package/package.json +20 -2
  93. package/src/bind.ts +30 -2
  94. package/src/boundaries.test.ts +54 -1
  95. package/src/boundary.ts +24 -8
  96. package/src/clear-remove.test.ts +85 -0
  97. package/src/client-only.test.ts +140 -0
  98. package/src/client-only.ts +65 -0
  99. package/src/component-empty-return.test.ts +123 -0
  100. package/src/debug.ts +12 -1
  101. package/src/dom-events.ts +32 -0
  102. package/src/dom-props-consistency.test.ts +149 -0
  103. package/src/dom-props.test.ts +70 -0
  104. package/src/dom-props.ts +107 -0
  105. package/src/dynamic.ts +91 -14
  106. package/src/error.test.ts +76 -1
  107. package/src/error.ts +87 -22
  108. package/src/event-cleanup-growth.test.ts +128 -0
  109. package/src/events.test.ts +16 -3
  110. package/src/hmr.ts +8 -2
  111. package/src/index.ts +13 -1
  112. package/src/invariants.test.ts +252 -0
  113. package/src/list-fast-path.test.ts +83 -0
  114. package/src/list-reactive-item.test.ts +77 -0
  115. package/src/ops.ts +419 -60
  116. package/src/props.test.ts +61 -1
  117. package/src/ref-cleanup.test.ts +144 -0
  118. package/src/ref.ts +57 -8
  119. package/src/remove-listener-identity.test.ts +97 -0
  120. package/src/renderer.ts +3 -1
  121. package/src/select-hidden-subscription.test.ts +104 -0
  122. package/src/show-classlist.test.ts +205 -0
  123. package/src/show.ts +107 -0
  124. package/src/spread-stable-shape.test.ts +109 -0
  125. package/src/spread.test.ts +86 -0
  126. package/src/svg.test.ts +28 -1
  127. package/src/svg.ts +34 -0
@@ -0,0 +1,54 @@
1
+ import { VobsNode } from './fragment.cjs';
2
+
3
+ interface ShowProps {
4
+ /**
5
+ * 条件。要响应式就传 getter / 信号读取表达式
6
+ * (编译器会把动态 props 自动转成 getter,所以 JSX 里写 `when={open.value}` 即可)。
7
+ */
8
+ readonly when: unknown;
9
+ /**
10
+ * 要显示的内容。**必须是单个元素**(`<div>` / `<input>` / 组件返回的元素都行)——
11
+ * 因为"保留挂载 + 切可见性"必须有一个宿主元素来挂 `hidden`/`inert`,
12
+ * 而框架**不会替你插包裹层**(包裹层会改变 flex/grid 布局,是更隐蔽的坑)。
13
+ */
14
+ readonly children?: VobsNode | (() => VobsNode | null | undefined);
15
+ /** 隐藏时额外挂上的类名(可选)。默认只用 `hidden` + `inert`。 */
16
+ readonly hiddenClass?: string;
17
+ }
18
+ /**
19
+ * 条件显示,**保留子树挂载状态**(不重建)。
20
+ *
21
+ * ## 为什么需要它(外部踩坑文档 C 条,★★)
22
+ *
23
+ * `insertDynamic` 按引用比较(`next === current`):条件表达式一变化就**重建子树**。
24
+ * 后果是条件分支里的输入控件**每敲一个字符就丢焦点**、滚动位置与内部状态全丢。
25
+ *
26
+ * 文档给出的对策是「固定渲染 + 响应式 class 显隐」,但那是**人肉纪律** ——
27
+ * 每个页面都要记得这么写。`Show` 把它变成一行:
28
+ *
29
+ * ```tsx
30
+ * <Show when={open.value} hiddenClass="is-hidden">
31
+ * <input /> {/* 条件怎么变,这个 input 都不会被重建,焦点与状态保持 *\/}
32
+ * </Show>
33
+ * ```
34
+ *
35
+ * 与 `{cond ? <A/> : <B/>}` 的分工:
36
+ * - 要**保留状态**(输入框、滚动位置、内部子系统)→ 用 `Show`
37
+ * - 要**真正卸载**(省内存、必须销毁副作用)→ 用 JSX 条件表达式(编译期生成条件工厂)
38
+ *
39
+ * ## 隐藏时同时挂 `inert`
40
+ *
41
+ * 只设 `hidden` 在部分场景下元素仍可被 Tab 到(详见 `@vobs/layout` 的同类处理)。
42
+ * `inert` 把子树移出**焦点顺序与无障碍树**。旧浏览器不支持时 `inert=""` 只是个
43
+ * 无害的未知属性,`hidden` 仍在兜底。
44
+ *
45
+ * ## 边界
46
+ *
47
+ * 子节点不是单个元素(文本 / fragment / 多个节点)时**抛出并说明原因** ——
48
+ * 不静默退化成"卸载"(那会让用户以为状态保住了,实际每变化一次就重建,
49
+ * 正是 C 条那个 bug 的形态)。需要保留状态就请把内容包在一个元素里。
50
+ */
51
+ declare function Show(props: ShowProps): VobsNode;
52
+ declare function readMaybeGetter<T>(value: T | (() => T)): T;
53
+
54
+ export { Show, type ShowProps, readMaybeGetter };
package/dist/show.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { VobsNode } from './fragment.js';
2
+
3
+ interface ShowProps {
4
+ /**
5
+ * 条件。要响应式就传 getter / 信号读取表达式
6
+ * (编译器会把动态 props 自动转成 getter,所以 JSX 里写 `when={open.value}` 即可)。
7
+ */
8
+ readonly when: unknown;
9
+ /**
10
+ * 要显示的内容。**必须是单个元素**(`<div>` / `<input>` / 组件返回的元素都行)——
11
+ * 因为"保留挂载 + 切可见性"必须有一个宿主元素来挂 `hidden`/`inert`,
12
+ * 而框架**不会替你插包裹层**(包裹层会改变 flex/grid 布局,是更隐蔽的坑)。
13
+ */
14
+ readonly children?: VobsNode | (() => VobsNode | null | undefined);
15
+ /** 隐藏时额外挂上的类名(可选)。默认只用 `hidden` + `inert`。 */
16
+ readonly hiddenClass?: string;
17
+ }
18
+ /**
19
+ * 条件显示,**保留子树挂载状态**(不重建)。
20
+ *
21
+ * ## 为什么需要它(外部踩坑文档 C 条,★★)
22
+ *
23
+ * `insertDynamic` 按引用比较(`next === current`):条件表达式一变化就**重建子树**。
24
+ * 后果是条件分支里的输入控件**每敲一个字符就丢焦点**、滚动位置与内部状态全丢。
25
+ *
26
+ * 文档给出的对策是「固定渲染 + 响应式 class 显隐」,但那是**人肉纪律** ——
27
+ * 每个页面都要记得这么写。`Show` 把它变成一行:
28
+ *
29
+ * ```tsx
30
+ * <Show when={open.value} hiddenClass="is-hidden">
31
+ * <input /> {/* 条件怎么变,这个 input 都不会被重建,焦点与状态保持 *\/}
32
+ * </Show>
33
+ * ```
34
+ *
35
+ * 与 `{cond ? <A/> : <B/>}` 的分工:
36
+ * - 要**保留状态**(输入框、滚动位置、内部子系统)→ 用 `Show`
37
+ * - 要**真正卸载**(省内存、必须销毁副作用)→ 用 JSX 条件表达式(编译期生成条件工厂)
38
+ *
39
+ * ## 隐藏时同时挂 `inert`
40
+ *
41
+ * 只设 `hidden` 在部分场景下元素仍可被 Tab 到(详见 `@vobs/layout` 的同类处理)。
42
+ * `inert` 把子树移出**焦点顺序与无障碍树**。旧浏览器不支持时 `inert=""` 只是个
43
+ * 无害的未知属性,`hidden` 仍在兜底。
44
+ *
45
+ * ## 边界
46
+ *
47
+ * 子节点不是单个元素(文本 / fragment / 多个节点)时**抛出并说明原因** ——
48
+ * 不静默退化成"卸载"(那会让用户以为状态保住了,实际每变化一次就重建,
49
+ * 正是 C 条那个 bug 的形态)。需要保留状态就请把内容包在一个元素里。
50
+ */
51
+ declare function Show(props: ShowProps): VobsNode;
52
+ declare function readMaybeGetter<T>(value: T | (() => T)): T;
53
+
54
+ export { Show, type ShowProps, readMaybeGetter };
package/dist/show.js ADDED
@@ -0,0 +1,171 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // packages/runtime/src/show.ts
5
+ import { effect as effect2 } from "@vobs/reactivity";
6
+
7
+ // packages/runtime/src/fragment.ts
8
+ import { getCurrentOwner as getCurrentOwner3 } from "@vobs/reactivity";
9
+
10
+ // packages/runtime/src/ops.ts
11
+ import { createOwner, effect, getCurrentOwner as getCurrentOwner2, setOwnerDebugName, untrack } from "@vobs/reactivity";
12
+
13
+ // packages/runtime/src/debug.ts
14
+ var activeRuntimeDebugHooks = null;
15
+ function getRuntimeDebugHooks() {
16
+ return activeRuntimeDebugHooks;
17
+ }
18
+ __name(getRuntimeDebugHooks, "getRuntimeDebugHooks");
19
+ function invokeRuntimeDebug(name, ...args) {
20
+ const callback = activeRuntimeDebugHooks?.[name];
21
+ if (!callback) return;
22
+ try {
23
+ callback(...args);
24
+ } catch {
25
+ }
26
+ }
27
+ __name(invokeRuntimeDebug, "invokeRuntimeDebug");
28
+ function readDebugValue(read) {
29
+ try {
30
+ return read();
31
+ } catch {
32
+ return "[Uninspectable]";
33
+ }
34
+ }
35
+ __name(readDebugValue, "readDebugValue");
36
+ function describeDebugNode(node) {
37
+ if (!node || typeof node !== "object") return "node";
38
+ const value = node;
39
+ const name = typeof value.tagName === "string" ? value.tagName.toLowerCase() : typeof value.nodeName === "string" ? value.nodeName.toLowerCase() : "node";
40
+ const id = typeof value.id === "string" && value.id ? `#${value.id}` : "";
41
+ const className = typeof value.className === "string" && value.className ? `.${value.className.trim().split(/\s+/).filter(Boolean).join(".")}` : "";
42
+ return `${name}${id}${className}`;
43
+ }
44
+ __name(describeDebugNode, "describeDebugNode");
45
+
46
+ // packages/runtime/src/hmr.ts
47
+ var globalTarget = globalThis;
48
+ var hmrGlobal = globalTarget.__VOBS_HMR__ ?? { modules: /* @__PURE__ */ new Map(), states: /* @__PURE__ */ new Map() };
49
+ globalTarget.__VOBS_HMR__ = hmrGlobal;
50
+
51
+ // packages/runtime/src/ref.ts
52
+ import { getCurrentOwner } from "@vobs/reactivity";
53
+
54
+ // packages/runtime/src/ops.ts
55
+ var currentRenderer = null;
56
+ function getRenderer() {
57
+ if (!currentRenderer) {
58
+ throw new Error("\u6E32\u67D3\u5668\u672A\u521D\u59CB\u5316");
59
+ }
60
+ return currentRenderer;
61
+ }
62
+ __name(getRenderer, "getRenderer");
63
+ function setProperty(node, key, value) {
64
+ const previousValue = getRuntimeDebugHooks() ? readDebugValue(() => Reflect.get(node, key)) : void 0;
65
+ getRenderer().setProperty(node, PROPERTY_IDL_NAMES[key] ?? key, value);
66
+ if (key === "value" && node.tagName === "SELECT") {
67
+ scheduleSelectValueSync(node, value);
68
+ }
69
+ if (getRuntimeDebugHooks()) {
70
+ invokeRuntimeDebug("domMutation", {
71
+ operation: "property",
72
+ target: describeDebugNode(node),
73
+ key,
74
+ previousValue,
75
+ nextValue: value
76
+ });
77
+ }
78
+ }
79
+ __name(setProperty, "setProperty");
80
+ var pendingSelectValues = /* @__PURE__ */ new WeakMap();
81
+ var selectSyncScheduled = /* @__PURE__ */ new WeakSet();
82
+ function scheduleSelectValueSync(node, value) {
83
+ pendingSelectValues.set(node, value);
84
+ if (selectSyncScheduled.has(node)) return;
85
+ selectSyncScheduled.add(node);
86
+ const renderer = getRenderer();
87
+ queueMicrotask(() => {
88
+ selectSyncScheduled.delete(node);
89
+ if (!pendingSelectValues.has(node)) return;
90
+ const pending = pendingSelectValues.get(node);
91
+ pendingSelectValues.delete(node);
92
+ renderer.setProperty(node, "value", pending);
93
+ });
94
+ }
95
+ __name(scheduleSelectValueSync, "scheduleSelectValueSync");
96
+ function setAttribute(node, key, value) {
97
+ const previousValue = getRuntimeDebugHooks() ? readDebugValue(() => typeof node.getAttribute === "function" ? node.getAttribute(key) : void 0) : void 0;
98
+ getRenderer().setAttribute(node, key, value);
99
+ if (getRuntimeDebugHooks()) {
100
+ invokeRuntimeDebug("domMutation", {
101
+ operation: "attribute",
102
+ target: describeDebugNode(node),
103
+ key,
104
+ previousValue,
105
+ nextValue: value
106
+ });
107
+ }
108
+ }
109
+ __name(setAttribute, "setAttribute");
110
+ function removeAttribute(node, key) {
111
+ const renderer = getRenderer();
112
+ if (renderer.removeAttribute) {
113
+ renderer.removeAttribute(node, key);
114
+ return;
115
+ }
116
+ renderer.setAttribute(node, key, "");
117
+ }
118
+ __name(removeAttribute, "removeAttribute");
119
+ var PROPERTY_IDL_NAMES = {
120
+ autoFocus: "autofocus"
121
+ };
122
+
123
+ // packages/runtime/src/fragment.ts
124
+ function isVobsFragment(value) {
125
+ return Boolean(value) && typeof value === "object" && value.kind === "vobs-fragment";
126
+ }
127
+ __name(isVobsFragment, "isVobsFragment");
128
+
129
+ // packages/runtime/src/show.ts
130
+ function Show(props) {
131
+ const resolved = typeof props.children === "function" ? props.children() : props.children;
132
+ if (resolved === null || resolved === void 0 || resolved === false) {
133
+ throw new Error("Vobs Show: children \u4E0D\u80FD\u4E3A\u7A7A \u2014\u2014 \u5B83\u9700\u8981\u4E00\u4E2A\u5BBF\u4E3B\u5143\u7D20\u6765\u6302 hidden/inert\u3002");
134
+ }
135
+ const node = resolved;
136
+ if (isVobsFragment(node) || node.nodeType !== 1) {
137
+ throw new Error(
138
+ "Vobs Show: children \u5FC5\u987B\u662F\u5355\u4E2A\u5143\u7D20\u3002\u300C\u4FDD\u7559\u6302\u8F7D + \u5207\u53EF\u89C1\u6027\u300D\u9700\u8981\u5BBF\u4E3B\u5143\u7D20\uFF0C\u800C\u6846\u67B6\u4E0D\u4F1A\u66FF\u4F60\u63D2\u5305\u88F9\u5C42\uFF08\u4F1A\u6539\u5E03\u5C40\uFF09\u3002\u8BF7\u628A\u5185\u5BB9\u5305\u8FDB\u4E00\u4E2A\u5143\u7D20\uFF0C\u4F8B\u5982 <Show when={\u2026}><div>\u2026</div></Show>\uFF1B\u82E5\u786E\u5B9E\u9700\u8981\u5378\u8F7D\u8BED\u4E49\uFF0C\u8BF7\u6539\u7528 JSX \u6761\u4EF6\u8868\u8FBE\u5F0F {cond ? <A/> : <B/>}\u3002"
139
+ );
140
+ }
141
+ const element = node;
142
+ effect2(() => {
143
+ const visible = Boolean(readMaybeGetter(props.when));
144
+ setProperty(element, "hidden", !visible);
145
+ if (visible) removeAttribute(element, "inert");
146
+ else setAttribute(element, "inert", "");
147
+ if (props.hiddenClass !== void 0) applyExtraClass(element, props.hiddenClass, !visible);
148
+ });
149
+ return element;
150
+ }
151
+ __name(Show, "Show");
152
+ function readMaybeGetter(value) {
153
+ return typeof value === "function" ? value() : value;
154
+ }
155
+ __name(readMaybeGetter, "readMaybeGetter");
156
+ function applyExtraClass(element, className, on) {
157
+ const names = element.getAttribute("class")?.split(/\s+/u).filter(Boolean) ?? [];
158
+ if (on) {
159
+ if (names.includes(className)) return;
160
+ setAttribute(element, "class", [...names, className].join(" "));
161
+ return;
162
+ }
163
+ if (!names.includes(className)) return;
164
+ setAttribute(element, "class", names.filter((name) => name !== className).join(" "));
165
+ }
166
+ __name(applyExtraClass, "applyExtraClass");
167
+ export {
168
+ Show,
169
+ readMaybeGetter
170
+ };
171
+ //# sourceMappingURL=show.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/show.ts","../src/fragment.ts","../src/ops.ts","../src/debug.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { effect } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\nimport { removeAttribute, setAttribute, setProperty } from './ops'\n\nexport interface ShowProps {\n /**\n * 条件。要响应式就传 getter / 信号读取表达式\n * (编译器会把动态 props 自动转成 getter,所以 JSX 里写 `when={open.value}` 即可)。\n */\n readonly when: unknown\n /**\n * 要显示的内容。**必须是单个元素**(`<div>` / `<input>` / 组件返回的元素都行)——\n * 因为\"保留挂载 + 切可见性\"必须有一个宿主元素来挂 `hidden`/`inert`,\n * 而框架**不会替你插包裹层**(包裹层会改变 flex/grid 布局,是更隐蔽的坑)。\n */\n readonly children?: VobsNode | (() => VobsNode | null | undefined)\n /** 隐藏时额外挂上的类名(可选)。默认只用 `hidden` + `inert`。 */\n readonly hiddenClass?: string\n}\n\n/**\n * 条件显示,**保留子树挂载状态**(不重建)。\n *\n * ## 为什么需要它(外部踩坑文档 C 条,★★)\n *\n * `insertDynamic` 按引用比较(`next === current`):条件表达式一变化就**重建子树**。\n * 后果是条件分支里的输入控件**每敲一个字符就丢焦点**、滚动位置与内部状态全丢。\n *\n * 文档给出的对策是「固定渲染 + 响应式 class 显隐」,但那是**人肉纪律** ——\n * 每个页面都要记得这么写。`Show` 把它变成一行:\n *\n * ```tsx\n * <Show when={open.value} hiddenClass=\"is-hidden\">\n * <input /> {/* 条件怎么变,这个 input 都不会被重建,焦点与状态保持 *\\/}\n * </Show>\n * ```\n *\n * 与 `{cond ? <A/> : <B/>}` 的分工:\n * - 要**保留状态**(输入框、滚动位置、内部子系统)→ 用 `Show`\n * - 要**真正卸载**(省内存、必须销毁副作用)→ 用 JSX 条件表达式(编译期生成条件工厂)\n *\n * ## 隐藏时同时挂 `inert`\n *\n * 只设 `hidden` 在部分场景下元素仍可被 Tab 到(详见 `@vobs/layout` 的同类处理)。\n * `inert` 把子树移出**焦点顺序与无障碍树**。旧浏览器不支持时 `inert=\"\"` 只是个\n * 无害的未知属性,`hidden` 仍在兜底。\n *\n * ## 边界\n *\n * 子节点不是单个元素(文本 / fragment / 多个节点)时**抛出并说明原因** ——\n * 不静默退化成\"卸载\"(那会让用户以为状态保住了,实际每变化一次就重建,\n * 正是 C 条那个 bug 的形态)。需要保留状态就请把内容包在一个元素里。\n */\nexport function Show(props: ShowProps): VobsNode {\n /*\n * 用 `unknown` 承接再判空:`props.children` 的声明类型不含 `false`,\n * 但运行时 `cond && <X/>` 这类写法确实会给出 `false` ——\n * 直接比较会被 TS 判成\"类型不可达\"(与 `createComponent` 同一处坑)。\n */\n const resolved: unknown = typeof props.children === 'function' ? props.children() : props.children\n if (resolved === null || resolved === undefined || resolved === false) {\n throw new Error('Vobs Show: children 不能为空 —— 它需要一个宿主元素来挂 hidden/inert。')\n }\n const node = resolved as VobsNode\n if (isVobsFragment(node) || (node as Element).nodeType !== 1) {\n throw new Error(\n 'Vobs Show: children 必须是单个元素。'\n + '「保留挂载 + 切可见性」需要宿主元素,而框架不会替你插包裹层(会改布局)。'\n + '请把内容包进一个元素,例如 <Show when={…}><div>…</div></Show>;'\n + '若确实需要卸载语义,请改用 JSX 条件表达式 {cond ? <A/> : <B/>}。'\n )\n }\n const element = node as Element\n\n effect(() => {\n const visible = Boolean(readMaybeGetter(props.when))\n /*\n * 可见性切换只碰这三个东西:`hidden`、`inert`、可选类名。\n * 不读任何信号(`when` 的读取在 effect 内、由调用方提供的 getter 完成),\n * 所以不会自我订阅。\n */\n setProperty(element, 'hidden', !visible)\n if (visible) removeAttribute(element, 'inert')\n else setAttribute(element, 'inert', '')\n if (props.hiddenClass !== undefined) applyExtraClass(element, props.hiddenClass, !visible)\n })\n\n return element\n}\n\nfunction readMaybeGetter<T>(value: T | (() => T)): T {\n return typeof value === 'function' ? (value as () => T)() : value\n}\n\n/** 按需增删一个类名(不影响作者写在 `class` 里的其它类)。 */\nfunction applyExtraClass(element: Element, className: string, on: boolean): void {\n const names = element.getAttribute('class')?.split(/\\s+/u).filter(Boolean) ?? []\n if (on) {\n if (names.includes(className)) return\n setAttribute(element, 'class', [...names, className].join(' '))\n return\n }\n if (!names.includes(className)) return\n setAttribute(element, 'class', names.filter(name => name !== className).join(' '))\n}\n\nexport { readMaybeGetter }\n","import { getCurrentOwner } from '@vobs/reactivity'\nimport { createComment, getRenderer } from './ops'\n\nexport interface VobsFragment {\n readonly kind: 'vobs-fragment'\n readonly start: Node\n readonly end: Node\n readonly mount: (parent: Node, anchor: Node | null) => void\n readonly unmount: (parent: Node) => void\n}\n\nexport type VobsNode = Node | VobsFragment\nexport type FragmentFactory = (parent: Node, anchor: Node) => void\n\nexport function createFragment(factory: FragmentFactory): VobsFragment {\n const start = createComment('vobs:fragment:start')\n const end = createComment('vobs:fragment:end')\n let parent: Node | null = null\n let initialized = false\n const owner = getCurrentOwner()\n\n const fragment: VobsFragment = {\n kind: 'vobs-fragment',\n start,\n end,\n mount(nextParent, anchor): void {\n if (parent && parent !== nextParent) {\n throw new Error('Vobs Fragment: 不能跨父节点移动 Fragment')\n }\n\n if (initialized) {\n moveRange(nextParent, start, end, anchor)\n return\n }\n\n const renderer = getRenderer()\n renderer.insertBefore(nextParent, start, anchor)\n renderer.insertBefore(nextParent, end, anchor)\n parent = nextParent\n initialized = true\n if (owner) owner.run(() => factory(nextParent, end))\n else factory(nextParent, end)\n },\n unmount(nextParent): void {\n if (!initialized || parent !== nextParent) {\n throw new Error('Vobs Fragment: Fragment 不属于指定父节点')\n }\n const renderer = getRenderer()\n let current = renderer.nextSibling(start)\n while (current && current !== end) {\n const next = renderer.nextSibling(current)\n renderer.removeChild(nextParent, current)\n current = next\n }\n renderer.removeChild(nextParent, start)\n renderer.removeChild(nextParent, end)\n parent = null\n initialized = false\n }\n }\n return fragment\n}\n\nexport function isVobsFragment(value: unknown): value is VobsFragment {\n return Boolean(value) && typeof value === 'object' && (value as VobsFragment).kind === 'vobs-fragment'\n}\n\nfunction moveRange(parent: Node, start: Node, end: Node, anchor: Node | null): void {\n const renderer = getRenderer()\n const nodes: Node[] = [start]\n let current = renderer.nextSibling(start)\n while (current) {\n nodes.push(current)\n if (current === end) break\n current = renderer.nextSibling(current)\n }\n if (nodes[nodes.length - 1] !== end) {\n throw new Error('Vobs Fragment: 找不到结束锚点')\n }\n for (const node of nodes) renderer.insertBefore(parent, node, anchor)\n}\n","// 编译产物调用的基础操作\n\nimport { createOwner, effect, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\nimport { domAttributeName, isPropertyName } from './dom-props'\nimport { resolveEventName } from './dom-events'\nimport { isSvgTag } from './svg'\nimport { formatVobsError } from './error'\nimport { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'\nimport {\n associateHmrInstance,\n markHmrInstanceMounted,\n registerHmrInstance,\n type HmrInstance\n} from './hmr'\nimport type { VobsRenderer } from './renderer'\nimport { setRef } from './ref'\n\ntype RuntimeRenderer = VobsRenderer<Node, Text, Element, Comment>\n\nlet currentRenderer: RuntimeRenderer | null = null\nconst nodeOwners = new WeakMap<object, Owner>()\ninterface EventBinding {\n readonly handler: EventListener\n readonly owner: Owner | null\n readonly original: EventListener\n}\nconst eventBindings = new WeakMap<object, Map<string, EventBinding>>()\n\n/*\n * 事件清理槽。目的:让**每个 (Owner, node, event) 只在 `Owner.cleanups` 里占一个槽**,\n * 重绑 handler 时只换槽里的间接引用,数组不增长(理由见 addEventListener)。\n *\n * 用 WeakMap<Owner, Map<key, handle>> 而不是往 Owner 上挂属性:\n * Owner 是 class 实例,挂新属性会把它推进字典模式(reactivity/src/owner.ts:30-50 记过这笔账)。\n * key 由 per-node 数字 id 与事件名拼成,避免每次重绑比较长字符串前缀。\n */\nconst eventCleanupSlots = new WeakMap<Owner, Map<string, EventBinding>>()\nconst eventCleanupNodeIds = new WeakMap<object, number>()\nlet nextEventCleanupNodeId = 0\n\nfunction eventCleanupKey(node: object, event: string): string {\n let id = eventCleanupNodeIds.get(node)\n if (id === undefined) {\n id = ++nextEventCleanupNodeId\n eventCleanupNodeIds.set(node, id)\n }\n return `${id}:${event}`\n}\n\nexport function setRenderer<\n NodeType,\n TextNode extends NodeType,\n ElementNode extends NodeType,\n CommentNode extends NodeType\n>(\n renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined\n): VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined {\n // 返回**上一份**渲染器(没有则是 undefined),调用方据此成对\"安装 / 还原\"。\n // 全局单例原来只装不还:一次 renderToString 之后就永久停在 SSR 渲染器上(见 vobs/src/app.ts)。\n const previous = currentRenderer as unknown as\n VobsRenderer<NodeType, TextNode, ElementNode, CommentNode> | undefined\n // 编译产物仍使用 DOM 节点声明;实际宿主类型由应用提供的渲染器决定。\n currentRenderer = (renderer as unknown as RuntimeRenderer) ?? null\n return previous\n}\n\nexport function getRenderer(): RuntimeRenderer {\n if (!currentRenderer) {\n throw new Error('渲染器未初始化')\n }\n return currentRenderer\n}\n\nexport function createText(content: string): Text {\n return getRenderer().createText(content)\n}\n\n/** SVG 命名空间与标签清单见 ./svg —— 编译期判定提升用的是同一份依据。 */\nexport { SVG_NAMESPACE } from './svg'\n\nexport function createElement(tag: string): Element {\n // SVG 标签走命名空间创建:document.createElement('rect') 产物是 HTMLUnknownElement,\n // 整棵 SVG 子树都不会渲染(无报错)。渲染器未实现 createSvgElement 时按 createElement\n // 兜底(旧自定义渲染器保持原行为)。\n if (isSvgTag(tag)) {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n }\n return getRenderer().createElement(tag)\n}\n\n/**\n * 在 SVG 命名空间里建元素,**不看标签名**。\n *\n * 给编译器用在 `<svg>` 祖先下那些与 HTML 同名的标签(a / title / style / script)——\n * 它们不在 SVG_TAGS 里,`createElement` 会按 HTML 建。只按名字判定命名空间是行不通的:\n * `<svg><a href=\"…\">` 会变成 HTML 锚点,而且不报错。\n *\n * 渲染器没实现 createSvgElement 时按 createElement 兜底(自定义渲染器保持原行为)。\n */\nexport function createSvgElement(tag: string): Element {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n return renderer.createElement(tag)\n}\n\nexport function createComment(content: string): Comment {\n return getRenderer().createComment(content)\n}\n\nexport function insertBefore(\n parent: Node,\n child: VobsNode,\n anchor: VobsNode | null\n): void {\n if (isVobsFragment(child)) {\n child.mount(parent, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n return\n }\n getRenderer().insertBefore(parent, child, isVobsFragment(anchor) ? anchor.start : anchor)\n markHmrInstanceMounted(child, parent)\n const nodeName = (child as Element).nodeName\n if (nodeName === 'OPTION' || nodeName === 'OPTGROUP') syncSelectValue(parent)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'insert',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n}\n\n/**\n * Select initial-value auto resync: `bindProperty(node, 'value', …)` registers the value\n * reader for `<select>` elements; whenever an `<option>` is inserted into the select\n * (directly or through an `<optgroup>`), the current bound value is re-applied. This\n * removes the last reason for apps to keep `ref + queueMicrotask` value-sync workarounds\n * for options that arrive after the value binding (e.g. asynchronously loaded lists).\n */\nconst selectValueReaders = new WeakMap<object, () => unknown>()\n\nexport function registerSelectValueBinding(select: Element, read: () => unknown): void {\n selectValueReaders.set(select, read)\n}\n\nfunction syncSelectValue(parent: Node): void {\n let current: Node | null = parent\n while (current !== null) {\n if ((current as Element).nodeName === 'SELECT') {\n const read = selectValueReaders.get(current)\n /*\n * 读取必须 `untrack` —— 这里是在**插入 option 的过程中同步**重放当前值,\n * 而插入往往发生在某个 effect 运行时(`insertList` / `insertDynamic` 都由 effect 驱动)。\n * 若那次读取被算进外层 effect 的依赖,该 effect 以后会被这个信号唤醒:\n * 一个**隐藏订阅** —— 依赖关系图上多一条没人打算建立的边,\n * 表现为\"插入选项时恰好重跑了某个不相关的 effect\",很难追。\n *\n * 这条绑定的订阅由 `bindProperty` 自己的 effect 负责(bind.ts),\n * 这里的重放只是把值**再写一次**,不需要也不应该建立依赖。\n */\n if (read !== undefined) setProperty(current as Element, 'value', untrack(read))\n return\n }\n current = current.parentNode\n }\n}\n\nexport function removeChild(\n parent: Node,\n child: VobsNode\n): void {\n /*\n * **先摘 DOM,再释放 Owner。**\n *\n * 原来是反过来的:`disposeNodeOwner(child)` 抛错(用户 cleanup 里抛)就永远走不到\n * 摘除那一步 —— DOM 里留着旧节点、它的 effect 却已经没了,屏幕上出现两棵树且没有提示。\n * 顺序反过来之后,即使清理抛错,DOM 也已一致;错误照旧抛给调用方。\n */\n if (isVobsFragment(child)) {\n child.unmount(parent)\n disposeNodeOwner(child)\n return\n }\n getRenderer().removeChild(parent, child)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'remove',\n target: describeDebugNode(child),\n parent: describeDebugNode(parent)\n })\n }\n disposeNodeOwner(child)\n}\n\nexport function setTextContent(\n node: Text,\n content: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => node.textContent)\n : undefined\n getRenderer().setTextContent(node, content)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'text',\n target: describeDebugNode(node),\n previousValue,\n nextValue: content\n })\n }\n}\n\nexport function setProperty(\n node: Element,\n key: string,\n value: unknown\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => Reflect.get(node, key))\n : undefined\n getRenderer().setProperty(node, PROPERTY_IDL_NAMES[key] ?? key, value)\n if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {\n scheduleSelectValueSync(node, value)\n }\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'property',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nconst pendingSelectValues = new WeakMap<Element, unknown>()\nconst selectSyncScheduled = new WeakSet<Element>()\n\n/**\n * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value\n * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值\n * (消费方此前只能用 ref + queueMicrotask 规避)。\n * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内\n * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,\n * 由 value 的绑定 effect 在后续信号更新中正常覆盖。\n */\nfunction scheduleSelectValueSync(node: Element, value: unknown): void {\n pendingSelectValues.set(node, value)\n if (selectSyncScheduled.has(node)) return\n selectSyncScheduled.add(node)\n /*\n * 在这一刻就把渲染器抓下来,**不要**在微任务里再 getRenderer():\n * 1) 应用销毁会把全局渲染器还原(没装过就是\"没有\"),那时微任务里的 getRenderer() 会抛\n * \"渲染器未初始化\" —— 抛在微任务里没人接,直接变成进程级 unhandled error(实测\n * `packages/ui/src/bind.test.ts` 就因为这条让 vitest 报 1 个 unhandled error,退出码非 0);\n * 2) 语义上也更对:节点是哪个渲染器造的,就用哪个渲染器写回(期间全局可能已经换人)。\n */\n const renderer = getRenderer()\n queueMicrotask(() => {\n selectSyncScheduled.delete(node)\n if (!pendingSelectValues.has(node)) return\n const pending = pendingSelectValues.get(node)\n pendingSelectValues.delete(node)\n renderer.setProperty(node, 'value', pending)\n })\n}\n\nexport function setAttribute(\n node: Element,\n key: string,\n value: string\n): void {\n const previousValue = getRuntimeDebugHooks()\n ? readDebugValue(() => typeof node.getAttribute === 'function' ? node.getAttribute(key) : undefined)\n : undefined\n getRenderer().setAttribute(node, key, value)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'attribute',\n target: describeDebugNode(node),\n key,\n previousValue,\n nextValue: value\n })\n }\n}\n\nexport function spreadProps(node: Element, props: Record<string, unknown>): void {\n applySpreadProps(node, null, props)\n}\n\n/** 删除一个 attribute。渲染器没实现时退回空串(属性仍在,但内容清空)—— 老自定义渲染器的降级行为。 */\nexport function removeAttribute(node: Element, key: string): void {\n const renderer = getRenderer()\n if (renderer.removeAttribute) {\n renderer.removeAttribute(node, key)\n return\n }\n renderer.setAttribute(node, key, '')\n}\n\n/** 布尔型 property 的「清除」值是 false,其余是空串(与 React 的移除语义一致)。 */\nconst BOOLEAN_PROPERTIES = new Set([\n 'checked', 'selected', 'disabled', 'multiple', 'readOnly', 'required',\n 'hidden', 'open', 'indeterminate', 'defaultChecked', 'muted', 'autoFocus'\n])\n\n/**\n * JSX 属性名 → **IDL 属性名**(`el[name] = value` 里的那个 name)。\n *\n * 多数名字两边相同,但少数不同;写错名字的后果是**静默的**:\n * `Reflect.set(el, 'autoFocus', …)` 只是给元素挂了一个不反射的 expando,\n * 既不产生 `autofocus` 属性、也不改变 IDL 属性 —— 于是 `autoFocus={false}` 毫无作用。\n * 实测(jsdom,与真实 DOM 同语义):\n * IDL 名 `autofocus`(全小写)→ has=true/idl=true,写 false 则 has=false/idl=false ← 反射正确\n * JSX 名 `autoFocus`(驼峰) → 只挂 expando,属性与 IDL 都不变\n * 这也解释了为什么 `disabled` / `checked` / `readOnly` / `tabIndex` 一直是对的:\n * 它们的 IDL 名恰好与 JSX 名相同。\n */\nconst PROPERTY_IDL_NAMES: Readonly<Record<string, string>> = {\n autoFocus: 'autofocus'\n}\n\n/**\n * 把一份 props 应用(或**增量**应用到)节点上。\n *\n * `previous` 为 null 表示首次应用(`spreadProps` 的老路径);否则只处理真正变化的键,\n * 并**移除**新对象里已经消失的键 —— 这正是原来缺的那一半:`{...props}` 只在创建时应用一次,\n * 之后对象里删掉的键会永远留在 DOM 上,而且改了的值也不会生效。\n */\nfunction applySpreadProps(\n node: Element,\n previous: Record<string, unknown> | null,\n next: Record<string, unknown>\n): void {\n for (const [key, value] of Object.entries(next)) {\n if (key === 'key') continue\n if (previous !== null && Object.is(previous[key], value)) continue\n\n if (key.startsWith('on')) {\n // 事件:先摘旧监听(值变了、或新值不再是函数),再挂新的。\n // 事件名必须过别名表:`onDoubleClick` 的真实事件名是 `dblclick`,\n // 直接小写会得到不存在的 `\"doubleclick\"`(回调永不触发且静默)。\n const eventName = resolveEventName(key) ?? key.slice(2).toLowerCase()\n const previousHandler = previous?.[key]\n if (typeof previousHandler === 'function') {\n removeEventListener(node, eventName, previousHandler as EventListener)\n }\n if (typeof value === 'function') {\n addEventListener(node, eventName, value as EventListener)\n }\n continue\n }\n if (key === 'ref') {\n setRef(node, value)\n continue\n }\n // null/undefined 表示\"不提供值\",**保持现状**(props.test.ts 把这条钉成了契约)。\n // 注意它有一个已知后果:键仍在 `next` 里时移除循环够不着(`key in next`),\n // 所以\"值从中变成 null\"不会清掉已有属性 —— 要清就得把键从对象里删掉。\n if (value === null || value === undefined) continue\n // property 键的 false 有语义(如 disabled={false} 必须清除),不能跳过;\n // attribute 键的 false 表示\"不设置\",与 HTML 语义一致。\n else if (isClassListKey(key)) applyClassList(node, value)\n else if (isPropertyName(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n\n if (previous === null) return\n for (const key of Object.keys(previous)) {\n if (key in next || key === 'key') continue\n if (key.startsWith('on')) {\n const eventName = resolveEventName(key) ?? key.slice(2).toLowerCase()\n const previousHandler = previous[key]\n if (typeof previousHandler === 'function') {\n removeEventListener(node, eventName, previousHandler as EventListener)\n }\n continue\n }\n if (key === 'ref') continue\n if (isPropertyName(key)) setProperty(node, key, BOOLEAN_PROPERTIES.has(key) ? false : '')\n else removeAttribute(node, domAttributeName(key))\n }\n}\n\n/**\n * 把 `{...props}` 绑定成**响应式**的:对象变了就增量应用,消失的键会被移除。\n *\n * 编译器对带展开的 JSX 属性发射这个而不是 `spreadProps` —— 后者只在创建时应用一次,\n * 于是「改了 props 不生效」「删掉的键留在 DOM 上」两个问题都**不报错**。\n *\n * ⚠️ `previous` 必须是**值的快照**,不能是 `next` 的引用。\n * 这里存引用时,下一轮 `applySpreadProps` 里的 `Object.is(previous[key], value)`\n * 读的是同一份值 → 恒等 → **整个 diff 短路**,移除循环也因 `key in next` 全跳过。\n * 而编译器为 `<div {...props}>` 发射的正是**身份稳定 + getter**的形状\n * (`bindSpreadProps(_el0, () => props)`,值经 getter 反应式求值),\n * 于是真实产物下\"改了 props 什么都不更新\"。\n *\n * 快照用浅拷贝即可,且**必须在应用之前取**:拷贝会把 getter 求值一次、转成数据属性,\n * 而 `applySpreadProps` 又会把 `next` 的每个键读一遍 —— 两者都在 effect 的追踪窗口内,\n * 所以信号依赖不会因为多这一遍而丢失。\n */\n/**\n * `classList` 每次贡献的类名(按元素记账,便于下次替换而不是叠加)。\n *\n * 为什么需要记账:`classList` 与 `class` 写的是**同一个** HTML 属性。\n * 若不记住上次由 `classList` 贡献了哪些,切换时要么越加越多、要么把作者的 `class` 抹掉。\n */\nconst classListContributions = new WeakMap<object, string[]>()\n\n/** `classList` 值 → 类名数组。支持对象 / 数组 / 字符串三种写法。 */\nexport function parseClassList(value: unknown): string[] {\n const names: string[] = []\n const push = (candidate: unknown): void => {\n if (typeof candidate === 'string') {\n for (const name of candidate.split(/\\s+/u)) if (name !== '') names.push(name)\n }\n }\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (entry !== null && typeof entry === 'object') {\n for (const [name, on] of Object.entries(entry)) if (on) push(name)\n } else push(entry)\n }\n } else if (value !== null && typeof value === 'object') {\n for (const [name, on] of Object.entries(value)) if (on) push(name)\n } else push(value)\n return names\n}\n\n/**\n * 应用 `classList`:与元素上已有的 `class` **合并**,并可反复切换。\n *\n * 存在的理由(外部踩坑文档 C 条):条件分支里的输入控件会丢焦点 ——\n * `insertDynamic` 按引用比较(`next === current`),写条件表达式就会重建子树。\n * 文档给出的对策是「固定渲染 + 响应式 class 显隐」,但此前只能手写字符串模板:\n *\n * ```tsx\n * <div class={open.value ? 'panel is-open' : 'panel'}> // 易漏、易写错\n * <div class=\"panel\" classList={{ 'is-open': open.value }}> // 现在:\n * ```\n *\n * `classList` 只负责它自己贡献的部分,作者写在 `class` 里的类名不受影响。\n */\nexport function applyClassList(node: Element, value: unknown): void {\n const next = parseClassList(value)\n const previous = classListContributions.get(node) ?? []\n classListContributions.set(node, next)\n const base = (node.getAttribute('class') ?? '')\n .split(/\\s+/u)\n .filter(name => name !== '' && !previous.includes(name))\n const merged = [...new Set([...base, ...next])]\n setAttribute(node, 'class', merged.join(' '))\n}\n\nfunction isClassListKey(key: string): boolean {\n return key === 'classList'\n}\n\nexport function bindSpreadProps(node: Element, source: () => Record<string, unknown>): void {\n let previous: Record<string, unknown> | null = null\n effect(() => {\n const next = source() ?? {}\n const snapshot = { ...next }\n applySpreadProps(node, previous, next)\n previous = snapshot\n })\n}\n\n/** Apply compile-time host properties in one renderer pass. */\nexport function setStaticProps(node: Element, props: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key.startsWith('on')) continue\n if (value === null || value === undefined) continue\n if (isClassListKey(key)) applyClassList(node, value)\n else if (isPropertyName(key)) setProperty(node, key, value)\n else if (value === false) continue\n else setAttribute(node, domAttributeName(key), key === 'style' && isStyleObject(value) ? formatStyle(value) : String(value))\n }\n}\n\n\nfunction isStyleObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction formatStyle(value: Record<string, unknown>): string {\n return Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined && entry !== false)\n .map(([key, entry]) => `${key.replace(/[A-Z]/gu, match => `-${match.toLowerCase()}`)}:${String(entry)}`).join(';')\n}\n\nexport function addEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const owner = getCurrentOwner()\n let bindings = eventBindings.get(node)\n if (!bindings) {\n bindings = new Map()\n eventBindings.set(node, bindings)\n }\n const previous = bindings.get(event)\n if (previous && previous.original === handler && previous.owner === owner) return\n if (previous) {\n // 旧监听先摘掉;Owner 清理槽由下面的 slots 原地续用,不在清理表里增删\n renderer.removeEventListener(node, event, previous.handler)\n }\n const listener = owner ? (reason: Event) => {\n // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。\n // 否则 owner.run 会抛\"已销毁的 Owner\",在事件流里制造无意义的错误噪音。\n if (owner.disposed) return\n try {\n owner.run(() => handler(reason))\n } catch (error) {\n const handled = owner.handleError(error)\n invokeRuntimeDebug('error', {\n error,\n owner,\n phase: 'event',\n handled,\n recovery: handled ? 'handled' : 'propagated'\n })\n /*\n * 被 owner 的 onError 处理掉之后,错误就只剩 debug 钩子这一个出口 ——\n * 没装 DevTools 时控制台一个字都没有:事件处理器抛错、界面没反应、无从查起。\n * 没被处理的情况下面会 rethrow(浏览器自己会报),所以只在「被吞下」时补控制台。\n */\n if (handled && getRuntimeDebugHooks()?.error === undefined) {\n console.error(formatVobsError(error, { includeStack: true }))\n }\n if (!handled) throw error\n }\n } : handler\n /*\n * 这个 (node,event) 在 Owner 清理表里只占**一个**槽,替换 handler 时**原地**换掉它做的事。\n *\n * 此前是在**每次** addEventListener 里无条件 `owner.onDispose(...)`,而 `Owner.cleanups`\n * 是只 push 的数组(reactivity/src/owner.ts:57,84)→ 换一次 handler 就多一条永不执行的\n * 清理项,旧的 handler 闭包也被一并扣住。实测(.artifacts/reports/runtime.supplement.md\n * 缺点 4 与既有报告缺点 1,两轮独立复现):500 次替换 → `cleanups.length === 500`;\n * 一轮 `{...props}` 换 onClick → 202。组件的 effect 每次重跑都会重绑事件,\n * 所以这是热路径上的**无界增长**(长寿命页面里等价于内存泄漏)。\n *\n * `binding` 是 `const`,所以槽里必须包一层可变的 `current` —— 换绑时改的是 `current`,\n * 数组长度不变、旧闭包被释放。\n */\n const binding: EventBinding = { handler: listener, owner, original: handler }\n bindings.set(event, binding)\n renderer.addEventListener(node, event, listener)\n if (!owner) return\n /*\n * **每个 (node,event) 在 Owner 清理表里只占一个槽。**\n *\n * 此前是在**每次** addEventListener 里无条件 `owner.onDispose(...)`,而 `Owner.cleanups`\n * 是只 push 的数组(reactivity/src/owner.ts:57,84)→ 换一次 handler 就多一条永不执行的\n * 清理项,旧的 handler 闭包也被一并扣住。实测(.artifacts/reports/runtime.supplement.md\n * 缺点 4 与既有报告缺点 1,两轮独立复现):500 次替换 → `cleanups.length === 500`;\n * 一轮 `{...props}` 换 onClick → 202。组件 effect 每次重跑都会重绑事件,\n * 所以这是热路径上的**无界增长**(长寿命页面里等价于内存泄漏)。\n *\n * 做法:清理槽里存的是**间接引用**(owner → (node,event) → 当前 handle),\n * 重绑时只换间接引用指向的东西,槽本身不增不减;最后一个引用被清掉时槽自然失效。\n * 键用 per-owner 的 `eventCleanupKeys` 映射成数字,避免长字符串前缀比较。\n */\n const key = eventCleanupKey(node, event)\n let slots = eventCleanupSlots.get(owner)\n if (!slots) {\n slots = new Map()\n eventCleanupSlots.set(owner, slots)\n }\n // 槽存的永远是\"当前该清哪一条绑定\",所以重绑/摘除后再绑都只是覆盖它\n const hasSlot = slots.has(key)\n slots.set(key, binding)\n if (hasSlot) return\n owner.onDispose(() => {\n const current = eventCleanupSlots.get(owner)?.get(key)\n if (!current) return\n eventCleanupSlots.get(owner)?.delete(key)\n const map = eventBindings.get(node)\n if (map?.get(event) === current) map.delete(event)\n renderer.removeEventListener(node, event, current.handler)\n })\n}\n\nexport function removeEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const map = eventBindings.get(node)\n const binding = map?.get(event)\n /*\n * **必须比 handler 身份**(DOM 的 `removeEventListener` 语义就是如此)。\n *\n * 原来这里忽略传入的 `handler`:只要 `(node,event)` 上有绑定就一律摘掉,\n * 并把绑定记录删掉。于是\"用另一个 handler 调 remove\"会把**别人的**绑定误摘 ——\n * 调用方以为只是取消自己那次(本来应当是 no-op),实际把当前生效的监听弄没了。\n *\n * 现在的行为:\n * - 有绑定且身份一致 → 摘掉(正常路径)\n * - 有绑定但身份不同 → **no-op**(不去动别人的绑定)\n * - 绑定记录里没有(例如由别处直接经 renderer 绑的)→ 按传入的 handler 兜底摘一次,\n * 保持与渲染器 API 直连时的可用性\n */\n if (binding) {\n if (binding.handler !== handler) return\n map?.delete(event)\n }\n renderer.removeEventListener(node, event, binding?.handler ?? handler)\n}\n\n/**\n * 清空容器。\n *\n * 原来只调渲染器的 clear(DOM 实现就是 `textContent = ''`)—— **被清掉子树的 Owner\n * 一个都不释放**:组件里的 effect 继续订阅信号(幽灵更新)、监听不解绑、onDispose 不跑。\n * 现在先把子树里能找到的 Owner 释放掉,再清 DOM。\n *\n * 覆盖范围如实说明:`nodeOwners` 只登记**组件输出节点**(`createComponent` 时\n * `associateNodeOwner`)。所以在组件边界上清理是完整的(组件 Owner 的销毁会级联它的\n * effect 与子 Owner);而在组件外直接 `bindText` 之类建立的 effect 挂在调用方当时的\n * Owner 上、不在节点表里,这里**清不到** —— 那部分归它的 Owner 管,通常由\n * `app.destroy()` 负责。要彻底解决需要节点表能反向枚举,代价更大,先不做。\n *\n * 清理抛错不影响 DOM 被清空(先释放、后清 DOM 的顺序也保证了这一点)。\n */\nexport function clear(container: Node): void {\n let firstError: unknown\n const walk = (node: Node): void => {\n try {\n disposeNodeOwner(node as unknown as VobsNode)\n } catch (error) {\n firstError ??= error\n }\n for (const child of Array.from(node.childNodes)) walk(child)\n }\n for (const child of Array.from(container.childNodes)) walk(child)\n\n getRenderer().clear(container)\n if (getRuntimeDebugHooks()) {\n invokeRuntimeDebug('domMutation', {\n operation: 'clear',\n target: describeDebugNode(container as unknown as VobsNode),\n parent: describeDebugNode(container as unknown as VobsNode)\n })\n }\n if (firstError) throw firstError\n}\n\ntype VobsComponent = (...args: any[]) => VobsNode\n\ntype ComponentProps<Component extends VobsComponent> = Component extends (\n props: infer Props\n) => VobsNode\n ? NonNullable<Props> extends object ? NonNullable<Props> : Record<string, never>\n : Record<string, never>\n\nexport function createComponent<Component extends VobsComponent>(\n component: Component,\n props: ComponentProps<Component>,\n source?: VobsSourceLocation\n): VobsNode {\n const owner = createOwner()\n const componentName = (component as typeof component & { displayName?: string }).displayName\n || component.name\n || 'anonymous'\n setOwnerDebugName(owner, source\n ? `${componentName} (${source.file}:${source.line}:${source.column})`\n : componentName)\n owner.onError(reason => {\n attachSourceLocation(reason, source)\n attachComponentContext(reason, componentName, owner.id)\n throw reason\n })\n // HMR 注册先于渲染作用域标记:注册清理必须跨热更新保活,不能被\n // disposeSince 当作上一轮渲染的清理释放掉。\n const hmrKey = (component as typeof component & { hmrKey?: string }).hmrKey\n let instance: HmrInstance | null = null\n if (hmrKey) {\n const separator = hmrKey.lastIndexOf(':')\n const moduleId = separator < 0 ? hmrKey : hmrKey.slice(0, separator)\n instance = { node: null as unknown as VobsNode, parent: null, refresh: () => refreshInstance() }\n const cleanup = registerHmrInstance(moduleId, instance)\n owner.onDispose(cleanup)\n }\n // 渲染作用域:组件 Owner 只承载 HMR 注册与错误处理,每轮渲染注册的 effect、\n // onDispose(含 portal 清理)与嵌套组件 Owner 都归属该轮作用域。HMR refresh\n // 重渲染前释放上一轮作用域,旧实例的 effect 不再订阅信号、portal 节点不再\n // 残留在 body 中每轮热更新叠加;组件 Owner 与 HMR 注册保活。\n const renderScope = owner.mark()\n /*\n * 局部类型放宽到运行时真实值域:组件返回 `null` / `undefined` / `false`\n * 都是合法 JSX 写法(`cond ? <X/> : null`、`cond && <X/>`),\n * 只是 `VobsComponent` 的声明把它写窄了。下面统一归一化成空注释节点。\n */\n /*\n * 用**独立变量**承接渲染结果再归一化。\n * 直接写 `let node: VobsNode | null | undefined | false` 再比较 `node === false` 时,\n * TS 的控制流分析会依据 `component(props)` 的声明类型把 `node` 收窄回 `VobsNode`,\n * 于是 `false` 分支被判成\"类型不可达\"而报错 —— 但运行时它确实会出现。\n */\n let rendered: unknown\n try {\n // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值\n // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,\n // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发\n // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。\n // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。\n rendered = owner.run(() => untrack(() => component(props)))\n } catch (error) {\n owner.dispose()\n attachSourceLocation(error, source)\n attachComponentContext(error, componentName, owner.id)\n throw error\n }\n /*\n * 组件返回 `null` / `undefined` / `false` 时用**注释节点**代替。\n *\n * 这是生产事故(Labelune 2026-09-30 发版黑屏)的根因:`createComponent` 把渲染结果\n * 直接当作 `nodeOwners` 这个 **WeakMap 的 key**,而 `null` / `undefined` 不是合法 key ——\n * `nodeOwners.set(null, owner)` 抛 `Invalid value used as weak map key`。\n * 后果特别贵:App 挂载即崩、**没有任何 JS 崩溃日志**、splash 兜底也失效,\n * 只能靠人肉纪律「组件永远返回真实节点」避免。\n *\n * 「组件顶层条件 return null」是合法的 JSX 写法,框架不该崩。\n * 注释节点是**渲染为空的既有原语**(fragment 与 dynamic 的标记都用它),\n * 视觉结果与返回 null 完全一致,但满足 `VobsNode` 契约 —— 于是挂载、HMR 刷新、\n * 节点替换、水合这些下游路径都不需要各自特判一个\"空\"分支。\n */\n const node: VobsNode = isRenderableNode(rendered) ? rendered : createComment('vobs:empty')\n associateNodeOwner(node, owner)\n if (instance) {\n instance.node = node\n associateHmrInstance(node, instance)\n }\n function refreshInstance(): void {\n // 组件已随旧渲染树一起卸载(如父级在同一次热更新中先完成刷新),\n // 无法也无需再刷新。\n if (owner.disposed) return\n const previous = instance!.node\n owner.disposeSince(renderScope)\n const next = owner.run(() => untrack(() => component(props)))\n const parent = instance!.parent\n if (parent) {\n // 先插入新树再卸载旧树:替换锚点始终取自仍在文档中的旧树,\n // fragment 与普通节点两种形态任意组合都能正确定位插入点。\n const anchor = isVobsFragment(previous) ? previous.start : previous\n if (isVobsFragment(next)) next.mount(parent, anchor)\n else getRenderer().insertBefore(parent, next, anchor)\n if (isVobsFragment(previous)) previous.unmount(parent)\n else getRenderer().removeChild(parent, previous)\n }\n nodeOwners.delete(previous as object)\n nodeOwners.set(next as object, owner)\n associateHmrInstance(next, instance!)\n instance!.node = next\n }\n return node\n}\n\nexport interface VobsSourceLocation {\n readonly file: string\n readonly line: number\n readonly column: number\n}\n\nexport interface VobsLocatedError extends Error {\n readonly vobsSource?: VobsSourceLocation\n readonly vobsComponent?: string\n readonly vobsOwnerId?: string\n}\n\nfunction attachSourceLocation(reason: unknown, source: VobsSourceLocation | undefined): void {\n if (!source || (!reason || (typeof reason !== 'object' && typeof reason !== 'function'))) return\n const error = reason as VobsLocatedError\n if (error.vobsSource) return\n try {\n Object.defineProperty(error, 'vobsSource', {\n configurable: true,\n enumerable: false,\n value: source,\n writable: false\n })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nfunction attachComponentContext(reason: unknown, component: string, ownerId: string): void {\n if (!reason || (typeof reason !== 'object' && typeof reason !== 'function')) return\n const error = reason as VobsLocatedError\n try {\n if (!error.vobsComponent) Object.defineProperty(error, 'vobsComponent', { configurable: true, enumerable: false, value: component, writable: false })\n if (!error.vobsOwnerId) Object.defineProperty(error, 'vobsOwnerId', { configurable: true, enumerable: false, value: ownerId, writable: false })\n } catch {\n // Frozen third-party errors still propagate with their original details.\n }\n}\n\nexport function createBlock(factory: () => VobsNode | null | undefined | false): VobsNode | null {\n const owner = createOwner()\n setOwnerDebugName(owner, 'dynamic')\n let node: VobsNode | null | undefined | false\n try {\n node = owner.run(factory)\n } catch (error) {\n owner.dispose()\n throw error\n }\n if (!node) {\n owner.dispose()\n return null\n }\n associateNodeOwner(node, owner)\n return node\n}\n\n/**\n * 组件渲染结果是否是**可用的节点值**。\n *\n * `VobsNode` 的真实值域是一个对象(`Node` / fragment / 自定义渲染器的宿主对象),\n * 而合法 JSX 写法可能返回 `null` / `undefined` / `false`(`cond ? <X/> : null`、`cond && <X/>`)。\n * 用一个守卫而不是直接比较,是因为 `component()` 的声明类型是 `VobsNode`,\n * TS 的控制流会把 `=== false` 之类的比较判成\"类型不可达\"。\n */\nfunction isRenderableNode(value: unknown): value is VobsNode {\n return (typeof value === 'object' && value !== null) || typeof value === 'function'\n}\n\nexport function associateNodeOwner(node: VobsNode, owner: Owner): void {\n /*\n * 防御:`node` 在类型上必然是 `VobsNode`,但运行时可能收到 `null`/`undefined`\n * (组件返回空、第三方渲染器返回非节点)。直接当 WeakMap key 会抛\n * `Invalid value used as weak map key` —— 那是**框架内部错误**,信息量为零,\n * 且会让 App 挂载静默崩溃。这条断言把它变成一条能指认原因的报错。\n */\n if (node === null || node === undefined || (typeof node !== 'object' && typeof node !== 'function')) {\n throw new Error(\n `Vobs: associateNodeOwner 收到非节点值(${String(node)})。`\n + '组件必须返回 VobsNode;返回 null/undefined 由 createComponent 转成空注释节点。'\n )\n }\n nodeOwners.set(node, owner)\n}\n\nexport function disposeNodeOwner(node: VobsNode): void {\n const owner = nodeOwners.get(node)\n if (!owner) return\n nodeOwners.delete(node)\n owner.dispose()\n}\n","import type { Owner } from '@vobs/reactivity'\n\nexport type RuntimeDebugEnvironment = 'client' | 'server'\n\n/**\n * Lightweight context copied onto debug events created synchronously inside a\n * Router loader, Effect or SSR render. It is intentionally optional so the\n * runtime remains useful without DevTools.\n */\nexport interface RuntimeDebugContext {\n readonly environment?: RuntimeDebugEnvironment\n readonly sessionId?: string\n readonly route?: string\n readonly navigationId?: number\n readonly dataRequestId?: number\n readonly updateId?: string\n readonly effectId?: string\n readonly source?: string\n}\n\nexport interface RuntimeHydrationMismatch {\n readonly kind: 'missing-node' | 'extra-node' | 'position' | 'content'\n readonly expected: string\n readonly actual: string\n readonly path: string\n readonly message: string\n}\n\nexport type RuntimeDomMutationOperation = 'text' | 'property' | 'attribute' | 'insert' | 'remove' | 'clear'\n\nexport interface RuntimeDomMutation {\n readonly operation: RuntimeDomMutationOperation\n readonly target: string\n readonly parent?: string\n readonly key?: string\n readonly previousValue?: unknown\n readonly nextValue?: unknown\n}\n\nexport interface RuntimeErrorEvent {\n readonly error: unknown\n readonly owner: Owner\n readonly phase: 'event' | 'boundary'\n readonly handled: boolean\n readonly recovery: 'propagated' | 'handled' | 'fallback' | 'retrying' | 'recovered'\n}\n\nexport interface RuntimeDebugHooks {\n domMutation?(mutation: RuntimeDomMutation): void\n error?(event: RuntimeErrorEvent): void\n hydrationMismatch?(event: RuntimeHydrationMismatch): void\n /**\n * 水合时**临时认领**了一个内容不为空的文本节点(服务端渲染了真实文本,客户端此刻要的是空文本:\n * 只能先认领、等绑定 effect 覆写)。这一步过去完全无声 —— 服务端 \"Ada\" 遇上客户端忘传 state\n * (会渲染 \"loading\")时就静默变成了后者。给出可观测出口,便于工具发现这类\"值被悄悄换掉\"。\n */\n hydrationProvisionalText?(event: RuntimeProvisionalText): void\n}\n\nexport interface RuntimeProvisionalText {\n readonly expected: string\n readonly actual: string\n}\n\nlet activeRuntimeDebugHooks: RuntimeDebugHooks | null = null\nlet activeRuntimeDebugContext: RuntimeDebugContext | null = null\n\nexport function setRuntimeDebugHooks(hooks: RuntimeDebugHooks | null): RuntimeDebugHooks | null {\n const previous = activeRuntimeDebugHooks\n activeRuntimeDebugHooks = hooks\n return previous\n}\n\nexport function getRuntimeDebugHooks(): RuntimeDebugHooks | null {\n return activeRuntimeDebugHooks\n}\n\nexport function getRuntimeDebugContext(): RuntimeDebugContext | null {\n return activeRuntimeDebugContext\n}\n\n/** Run a synchronous operation with trace context, preserving async renders. */\nexport function runWithRuntimeDebugContext<T>(context: RuntimeDebugContext, task: () => T): T {\n const previous = activeRuntimeDebugContext\n const next = { ...previous, ...context }\n activeRuntimeDebugContext = next\n let result: T\n try {\n result = task()\n } catch (error) {\n activeRuntimeDebugContext = previous\n throw error\n }\n if (isPromiseLike(result)) {\n return Promise.resolve(result).finally(() => {\n if (activeRuntimeDebugContext === next) activeRuntimeDebugContext = previous\n }) as T\n }\n activeRuntimeDebugContext = previous\n return result\n}\n\n/** Keep a context active across callback boundaries such as Effect execution. */\nexport function pushRuntimeDebugContext(context: RuntimeDebugContext): () => void {\n const previous = activeRuntimeDebugContext\n activeRuntimeDebugContext = { ...previous, ...context }\n let restored = false\n return () => {\n if (restored) return\n restored = true\n activeRuntimeDebugContext = previous\n }\n}\n\nexport function invokeRuntimeDebug<K extends keyof RuntimeDebugHooks>(\n name: K,\n ...args: Parameters<NonNullable<RuntimeDebugHooks[K]>>\n): void {\n const callback = activeRuntimeDebugHooks?.[name] as ((...values: unknown[]) => void) | undefined\n if (!callback) return\n try {\n callback(...args)\n } catch {\n // Debug tooling must never change runtime behavior.\n }\n}\n\nexport function readDebugValue(read: () => unknown): unknown {\n try {\n return read()\n } catch {\n return '[Uninspectable]'\n }\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return Boolean(value) && (typeof value === 'object' || typeof value === 'function')\n && typeof (value as { then?: unknown }).then === 'function'\n}\n\nexport function describeDebugNode(node: unknown): string {\n if (!node || typeof node !== 'object') return 'node'\n const value = node as {\n readonly nodeName?: unknown\n readonly tagName?: unknown\n readonly id?: unknown\n readonly className?: unknown\n }\n const name = typeof value.tagName === 'string'\n ? value.tagName.toLowerCase()\n : typeof value.nodeName === 'string' ? value.nodeName.toLowerCase() : 'node'\n const id = typeof value.id === 'string' && value.id ? `#${value.id}` : ''\n const className = typeof value.className === 'string' && value.className\n ? `.${value.className.trim().split(/\\s+/).filter(Boolean).join('.')}`\n : ''\n return `${name}${id}${className}`\n}\n","import type { VobsNode } from './fragment'\nimport { formatVobsError } from './error'\n\nexport type HmrComponent<Props extends object = Record<string, unknown>> =\n (props: Props) => VobsNode\n\nexport interface HmrStateStore {\n get<T>(key: string, initial: T | (() => T)): T\n set<T>(key: string, value: T): void\n has(key: string): boolean\n delete(key: string): void\n clear(): void\n}\n\nexport interface HmrInstance {\n node: VobsNode\n parent: Node | null\n refresh(): void\n}\n\ninterface HmrModuleState {\n readonly components: Map<string, HmrComponent>\n readonly state: Map<string, unknown>\n readonly instances: Set<HmrInstance>\n}\n\ninterface HmrGlobal {\n modules: Map<string, HmrModuleState>\n /** 编译器生成的 hmrStateRef 注册表:键为 `${moduleId}#${声明名}`,值跨模块重执行保活。 */\n states: Map<string, unknown>\n}\n\nconst globalTarget = globalThis as typeof globalThis & { __VOBS_HMR__?: HmrGlobal }\nconst hmrGlobal = globalTarget.__VOBS_HMR__ ?? { modules: new Map<string, HmrModuleState>(), states: new Map<string, unknown>() }\nglobalTarget.__VOBS_HMR__ = hmrGlobal\n\nexport function resolveComponent<Props extends object>(\n component: HmrComponent<Props>,\n moduleId: string,\n exportName: string\n): HmrComponent<Props> {\n const module = getModule(moduleId)\n const existing = module.components.get(exportName)\n if (existing) return existing as HmrComponent<Props>\n\n const proxy = ((props: Props) => {\n const current = (proxy as HmrComponent<Props> & { current: HmrComponent<Props> }).current\n return current(props)\n }) as HmrComponent<Props> & { current: HmrComponent<Props> }\n proxy.current = component\n Object.defineProperties(proxy, {\n displayName: { configurable: true, value: component.name || exportName },\n hmrKey: { configurable: false, value: `${moduleId}:${exportName}` }\n })\n module.components.set(exportName, proxy as HmrComponent)\n return proxy\n}\n\nexport function updateHmrModule(moduleId: string, nextModule: Record<string, unknown>): void {\n const module = hmrGlobal.modules.get(moduleId)\n if (!module) return\n let changed = false\n for (const [name, proxy] of module.components) {\n const next = nextModule[name]\n if (typeof next !== 'function') continue\n const hmrProxy = proxy as HmrComponent & { current: HmrComponent; displayName?: string }\n hmrProxy.current = next as HmrComponent\n Object.defineProperty(hmrProxy, 'displayName', { configurable: true, value: next.name || name })\n changed = true\n }\n if (!changed) return\n // 快照后正序遍历:实例在自身渲染开始前注册,祖先必然先于后代入列——\n // 祖先先刷新会 dispose 上一轮渲染作用域,其树内旧后代实例的 refresh\n // 命中已销毁守卫被跳过,避免同一组件在一次热更新中重渲染两次。\n // 同时 refresh 重渲染以新代码创建的全新实例不在快照中,天然不会再刷新。\n const instances = [...module.instances]\n for (const instance of instances) {\n try {\n instance.refresh()\n } catch (error) {\n /*\n * 静默吞掉会让开发者以为热更新成功了,而屏幕上是旧的 —— 最容易被当成\n * 「改了没生效」查半天。刷新失败本身不冒泡(下一次正常渲染会再抛),\n * 但必须留下记录。\n */\n console.error(formatVobsError(error, { includeStack: true }))\n }\n }\n}\n\nexport function disposeHmrModule(_moduleId: string): void {\n // State and component proxies intentionally survive module disposal.\n}\n\nexport function createHmrStateStore(moduleId: string): HmrStateStore {\n const state = getModule(moduleId).state\n return {\n get<T>(key: string, initial: T | (() => T)): T {\n if (!state.has(key)) state.set(key, typeof initial === 'function' ? (initial as () => T)() : initial)\n return state.get(key) as T\n },\n set<T>(key: string, value: T): void {\n state.set(key, value)\n },\n has: key => state.has(key),\n delete: key => { state.delete(key) },\n clear: () => { state.clear() }\n }\n}\n\n/**\n * 编译器为模块顶层 state() 声明生成的取值入口。键为 `${moduleId}#${声明名}`,\n * 与手动 store 键空间隔离。模块热更新重执行时复用既有信号实例:旧导入方持有的\n * 实例与新模块实例共享同一份状态,消除\"两份模块、两份状态\"导致的编辑不生效/页面半边失灵。\n */\nexport function hmrStateRef<T>(key: string, create: () => T): T {\n const states = hmrGlobal.states\n if (states.has(key)) return states.get(key) as T\n const value = create()\n states.set(key, value)\n return value\n}\n\nexport function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {\n const instances = getModule(moduleId).instances\n instances.add(instance)\n return () => instances.delete(instance)\n}\n\nexport function markHmrInstanceMounted(node: VobsNode, parent: Node): void {\n const instance = hmrInstances.get(node as object)\n if (instance) instance.parent = parent\n}\n\nfunction getModule(moduleId: string): HmrModuleState {\n let module = hmrGlobal.modules.get(moduleId)\n if (!module) {\n module = { components: new Map(), state: new Map(), instances: new Set() }\n hmrGlobal.modules.set(moduleId, module)\n }\n return module\n}\n\nconst hmrInstances = new WeakMap<object, HmrInstance>()\n\nexport function associateHmrInstance(node: VobsNode, instance: HmrInstance): void {\n hmrInstances.set(node as object, instance)\n}\n","import { getCurrentOwner, type Owner } from '@vobs/reactivity'\nimport { formatVobsError } from './error'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/*\n * 每个 (Owner, ref 目标) 只保留**一条**清理注册。\n *\n * 为什么需要:`ref` 会被先后绑到不同节点(同一个 ref 对象、节点被替换;\n * 或条件分支里换了宿主元素)。若每次都 `onDispose` 追加一条,销毁时按注册**逆序**执行,\n * 后注册的那条先把 `target.current` 清成 `null`,于是先注册那条的守卫\n * (`isObjectRef(target) && target.current !== node`)**恰好通过**,再清一次 ——\n * 最终读到 `null` 而不是当前节点。\n *\n * 用 WeakMap<Owner, WeakMap<target, cleanup>> 而不是往 Owner 上挂属性:\n * Owner 是 class 实例,挂新属性会把它推进字典模式(reactivity/src/owner.ts 记过这笔代价)。\n * target 可弱引用(对象),函数型 ref 也可弱引用,所以不阻止回收。\n */\nconst refCleanups = new WeakMap<Owner, WeakMap<object, () => void>>()\n\nfunction rememberRefCleanup(owner: Owner, target: object, cleanup: () => void): void {\n let byTarget = refCleanups.get(owner)\n if (!byTarget) {\n byTarget = new WeakMap()\n refCleanups.set(owner, byTarget)\n }\n const previous = byTarget.get(target)\n if (previous) owner.removeCleanup(previous)\n byTarget.set(target, cleanup)\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n if (!owner) return\n const cleanup = (): void => {\n /*\n * 清除 ref 时**必须自己吞掉异常**,不能让 user 的 ref 回调把 dispose 级联打断。\n *\n * `assignRef` 内部已经 try/catch + `console.error`(回调是用户代码,不该让挂载失败),\n * 但那条保护只覆盖\"赋值\"这一步 —— 而这里还有一个**可能抛错的前置判断**。\n * 按 React 语义,函数型 ref 在卸载时**本来就该收到 `null`**;若回调没判空\n * (如 `node => insertList(node, …)`),它会抛,而异常从 onDispose 冒出去会\n * **中断 Owner 的整个清理循环**(实测:/runtime 页面的 `<ul ref={attachList}>`)。\n * 这与 owner.ts 已修的\"子 Owner 抛错不能中断级联\"是同一类缺陷。\n */\n try {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n } catch (error) {\n console.error(formatVobsError(error, { includeStack: true }))\n }\n }\n rememberRefCleanup(owner, target as object, cleanup)\n owner.onDispose(cleanup)\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch (error) {\n /*\n * ref 回调是用户代码,挂载不该因为它失败 —— 但也不能**完全无声**:\n * 回调抛错时 ref 拿不到节点,界面看起来正常,用户完全不知道为什么。\n * 保持吞掉(不冒泡),但打一条控制台记录。\n */\n console.error(formatVobsError(error, { includeStack: true }))\n }\n}\n"],"mappings":"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","names":["effect","getCurrentOwner","getCurrentOwner","effect"]}
package/dist/svg.cjs ADDED
@@ -0,0 +1,102 @@
1
+ var __VOBS_CJS_FILE_URL = require("url").pathToFileURL(__filename).href;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // packages/runtime/src/svg.ts
23
+ var svg_exports = {};
24
+ __export(svg_exports, {
25
+ SVG_NAMESPACE: () => SVG_NAMESPACE,
26
+ SVG_TAGS: () => SVG_TAGS,
27
+ isSvgTag: () => isSvgTag
28
+ });
29
+ module.exports = __toCommonJS(svg_exports);
30
+ var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
31
+ var SVG_TAGS = /* @__PURE__ */ new Set([
32
+ "animate",
33
+ "animateMotion",
34
+ "animateTransform",
35
+ "circle",
36
+ "clipPath",
37
+ "defs",
38
+ "desc",
39
+ "ellipse",
40
+ "feBlend",
41
+ "feColorMatrix",
42
+ "feComponentTransfer",
43
+ "feComposite",
44
+ "feConvolveMatrix",
45
+ "feDiffuseLighting",
46
+ "feDisplacementMap",
47
+ "feDistantLight",
48
+ "feDropShadow",
49
+ "feFlood",
50
+ "feFuncA",
51
+ "feFuncB",
52
+ "feFuncG",
53
+ "feFuncR",
54
+ "feGaussianBlur",
55
+ "feImage",
56
+ "feMerge",
57
+ "feMergeNode",
58
+ "feMorphology",
59
+ "feOffset",
60
+ "fePointLight",
61
+ "feSpecularLighting",
62
+ "feSpotLight",
63
+ "feTile",
64
+ "feTurbulence",
65
+ "filter",
66
+ "foreignObject",
67
+ "g",
68
+ "image",
69
+ "line",
70
+ "linearGradient",
71
+ "marker",
72
+ "mask",
73
+ "metadata",
74
+ "mpath",
75
+ "path",
76
+ "pattern",
77
+ "polygon",
78
+ "polyline",
79
+ "radialGradient",
80
+ "rect",
81
+ "set",
82
+ "stop",
83
+ "svg",
84
+ "switch",
85
+ "symbol",
86
+ "text",
87
+ "textPath",
88
+ "tspan",
89
+ "use",
90
+ "view"
91
+ ]);
92
+ function isSvgTag(tag) {
93
+ return SVG_TAGS.has(tag);
94
+ }
95
+ __name(isSvgTag, "isSvgTag");
96
+ // Annotate the CommonJS export names for ESM import in node:
97
+ 0 && (module.exports = {
98
+ SVG_NAMESPACE,
99
+ SVG_TAGS,
100
+ isSvgTag
101
+ });
102
+ //# sourceMappingURL=svg.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/svg.ts"],"sourcesContent":["/**\n * SVG 标签与命名空间 —— 编译期判定提升、运行期决定怎么建节点的**同一份依据**。\n *\n * 两处都需要知道「这个名字是不是 SVG 标签」:\n * - 运行期:`document.createElement('rect')` 得到的是 HTMLUnknownElement,\n * 整棵 SVG 子树都不渲染(且没有报错),所以必须走 createElementNS\n * - 编译期:静态子树被提升成 HTML 模板串再 cloneNode,而模板串是**用 HTML 解析器**\n * 解析的 —— `<g>`、`<rect>` 同样变成 HTMLUnknownElement。这条路径漏了整整一个大版本\n * (1.7.4 只修了 createElement 那条入口,svg.test.ts 也只测那条)。\n */\nexport const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'\n\n/**\n * SVG 标签全集(= TypeScript SVGElementTagNameMap 的键,运行时分发依据)。\n * 与 React 的运行时清单同思路:只含纯 SVG 标签,刻意排除与 HTML 同名的\n * a/script/style/title(它们在 JSX 类型里归 HTMLElement,按 HTML 创建)。\n */\nexport const SVG_TAGS: ReadonlySet<string> = new Set([\n 'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'defs', 'desc',\n 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite',\n 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight',\n 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR',\n 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset',\n 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence',\n 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask',\n 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient',\n 'rect', 'set', 'stop', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tspan',\n 'use', 'view'\n])\n\n/** 是否是 SVG 专属标签(按名字判定,不看父级)。 */\nexport function isSvgTag(tag: string): boolean {\n return SVG_TAGS.has(tag)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,IAAM,gBAAgB;AAOtB,IAAM,WAAgC,oBAAI,IAAI;AAAA,EACnD;AAAA,EAAW;AAAA,EAAiB;AAAA,EAAoB;AAAA,EAAU;AAAA,EAAY;AAAA,EAAQ;AAAA,EAC9E;AAAA,EAAW;AAAA,EAAW;AAAA,EAAiB;AAAA,EAAuB;AAAA,EAC9D;AAAA,EAAoB;AAAA,EAAqB;AAAA,EAAqB;AAAA,EAC9D;AAAA,EAAgB;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAC5D;AAAA,EAAkB;AAAA,EAAW;AAAA,EAAW;AAAA,EAAe;AAAA,EAAgB;AAAA,EACvE;AAAA,EAAgB;AAAA,EAAsB;AAAA,EAAe;AAAA,EAAU;AAAA,EAC/D;AAAA,EAAU;AAAA,EAAiB;AAAA,EAAK;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAkB;AAAA,EAAU;AAAA,EAC7E;AAAA,EAAY;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAW;AAAA,EAAW;AAAA,EAAY;AAAA,EAC/D;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAU;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAY;AAAA,EACtE;AAAA,EAAO;AACT,CAAC;AAGM,SAAS,SAAS,KAAsB;AAC7C,SAAO,SAAS,IAAI,GAAG;AACzB;AAFgB;","names":[]}
package/dist/svg.d.cts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SVG 标签与命名空间 —— 编译期判定提升、运行期决定怎么建节点的**同一份依据**。
3
+ *
4
+ * 两处都需要知道「这个名字是不是 SVG 标签」:
5
+ * - 运行期:`document.createElement('rect')` 得到的是 HTMLUnknownElement,
6
+ * 整棵 SVG 子树都不渲染(且没有报错),所以必须走 createElementNS
7
+ * - 编译期:静态子树被提升成 HTML 模板串再 cloneNode,而模板串是**用 HTML 解析器**
8
+ * 解析的 —— `<g>`、`<rect>` 同样变成 HTMLUnknownElement。这条路径漏了整整一个大版本
9
+ * (1.7.4 只修了 createElement 那条入口,svg.test.ts 也只测那条)。
10
+ */
11
+ declare const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
12
+ /**
13
+ * SVG 标签全集(= TypeScript SVGElementTagNameMap 的键,运行时分发依据)。
14
+ * 与 React 的运行时清单同思路:只含纯 SVG 标签,刻意排除与 HTML 同名的
15
+ * a/script/style/title(它们在 JSX 类型里归 HTMLElement,按 HTML 创建)。
16
+ */
17
+ declare const SVG_TAGS: ReadonlySet<string>;
18
+ /** 是否是 SVG 专属标签(按名字判定,不看父级)。 */
19
+ declare function isSvgTag(tag: string): boolean;
20
+
21
+ export { SVG_NAMESPACE, SVG_TAGS, isSvgTag };
package/dist/svg.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SVG 标签与命名空间 —— 编译期判定提升、运行期决定怎么建节点的**同一份依据**。
3
+ *
4
+ * 两处都需要知道「这个名字是不是 SVG 标签」:
5
+ * - 运行期:`document.createElement('rect')` 得到的是 HTMLUnknownElement,
6
+ * 整棵 SVG 子树都不渲染(且没有报错),所以必须走 createElementNS
7
+ * - 编译期:静态子树被提升成 HTML 模板串再 cloneNode,而模板串是**用 HTML 解析器**
8
+ * 解析的 —— `<g>`、`<rect>` 同样变成 HTMLUnknownElement。这条路径漏了整整一个大版本
9
+ * (1.7.4 只修了 createElement 那条入口,svg.test.ts 也只测那条)。
10
+ */
11
+ declare const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
12
+ /**
13
+ * SVG 标签全集(= TypeScript SVGElementTagNameMap 的键,运行时分发依据)。
14
+ * 与 React 的运行时清单同思路:只含纯 SVG 标签,刻意排除与 HTML 同名的
15
+ * a/script/style/title(它们在 JSX 类型里归 HTMLElement,按 HTML 创建)。
16
+ */
17
+ declare const SVG_TAGS: ReadonlySet<string>;
18
+ /** 是否是 SVG 专属标签(按名字判定,不看父级)。 */
19
+ declare function isSvgTag(tag: string): boolean;
20
+
21
+ export { SVG_NAMESPACE, SVG_TAGS, isSvgTag };