@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.
- package/README.md +1 -1
- package/dist/async-boundary.cjs +14 -4
- package/dist/async-boundary.cjs.map +1 -1
- package/dist/async-boundary.js +17 -7
- package/dist/async-boundary.js.map +1 -1
- package/dist/bind.cjs +43 -3
- package/dist/bind.cjs.map +1 -1
- package/dist/bind.js +48 -8
- package/dist/bind.js.map +1 -1
- package/dist/boundary.cjs +159 -84
- package/dist/boundary.cjs.map +1 -1
- package/dist/boundary.js +162 -87
- package/dist/boundary.js.map +1 -1
- package/dist/client-only.cjs +323 -0
- package/dist/client-only.cjs.map +1 -0
- package/dist/client-only.d.cts +42 -0
- package/dist/client-only.d.ts +42 -0
- package/dist/client-only.js +299 -0
- package/dist/client-only.js.map +1 -0
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.cts +12 -2
- package/dist/debug.d.ts +12 -2
- package/dist/debug.js.map +1 -1
- package/dist/dom-events.cjs +46 -0
- package/dist/dom-events.cjs.map +1 -0
- package/dist/dom-events.d.cts +23 -0
- package/dist/dom-events.d.ts +23 -0
- package/dist/dom-events.js +21 -0
- package/dist/dom-events.js.map +1 -0
- package/dist/dom-props.cjs +154 -0
- package/dist/dom-props.cjs.map +1 -0
- package/dist/dom-props.d.cts +21 -0
- package/dist/dom-props.d.ts +21 -0
- package/dist/dom-props.js +128 -0
- package/dist/dom-props.js.map +1 -0
- package/dist/dynamic.cjs +68 -14
- package/dist/dynamic.cjs.map +1 -1
- package/dist/dynamic.js +73 -19
- package/dist/dynamic.js.map +1 -1
- package/dist/error-boundary.cjs +159 -84
- package/dist/error-boundary.cjs.map +1 -1
- package/dist/error-boundary.js +162 -87
- package/dist/error-boundary.js.map +1 -1
- package/dist/error.cjs +52 -20
- package/dist/error.cjs.map +1 -1
- package/dist/error.d.cts +8 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.js +52 -20
- package/dist/error.js.map +1 -1
- package/dist/fragment.cjs.map +1 -1
- package/dist/fragment.js +1 -1
- package/dist/fragment.js.map +1 -1
- package/dist/hmr.cjs +137 -1
- package/dist/hmr.cjs.map +1 -1
- package/dist/hmr.js +135 -1
- package/dist/hmr.js.map +1 -1
- package/dist/index.cjs +713 -236
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +715 -245
- package/dist/index.js.map +1 -1
- package/dist/ops.cjs +561 -107
- package/dist/ops.cjs.map +1 -1
- package/dist/ops.d.cts +49 -4
- package/dist/ops.d.ts +49 -4
- package/dist/ops.js +557 -108
- package/dist/ops.js.map +1 -1
- package/dist/profiler.cjs +14 -4
- package/dist/profiler.cjs.map +1 -1
- package/dist/profiler.js +17 -7
- package/dist/profiler.js.map +1 -1
- package/dist/ref.cjs +160 -5
- package/dist/ref.cjs.map +1 -1
- package/dist/ref.js +160 -5
- package/dist/ref.js.map +1 -1
- package/dist/renderer.cjs.map +1 -1
- package/dist/renderer.d.cts +2 -0
- package/dist/renderer.d.ts +2 -0
- package/dist/show.cjs +196 -0
- package/dist/show.cjs.map +1 -0
- package/dist/show.d.cts +54 -0
- package/dist/show.d.ts +54 -0
- package/dist/show.js +171 -0
- package/dist/show.js.map +1 -0
- package/dist/svg.cjs +102 -0
- package/dist/svg.cjs.map +1 -0
- package/dist/svg.d.cts +21 -0
- package/dist/svg.d.ts +21 -0
- package/dist/svg.js +76 -0
- package/dist/svg.js.map +1 -0
- package/package.json +20 -2
- package/src/bind.ts +30 -2
- package/src/boundaries.test.ts +54 -1
- package/src/boundary.ts +24 -8
- package/src/clear-remove.test.ts +85 -0
- package/src/client-only.test.ts +140 -0
- package/src/client-only.ts +65 -0
- package/src/component-empty-return.test.ts +123 -0
- package/src/debug.ts +12 -1
- package/src/dom-events.ts +32 -0
- package/src/dom-props-consistency.test.ts +149 -0
- package/src/dom-props.test.ts +70 -0
- package/src/dom-props.ts +107 -0
- package/src/dynamic.ts +91 -14
- package/src/error.test.ts +76 -1
- package/src/error.ts +87 -22
- package/src/event-cleanup-growth.test.ts +128 -0
- package/src/events.test.ts +16 -3
- package/src/hmr.ts +8 -2
- package/src/index.ts +13 -1
- package/src/invariants.test.ts +252 -0
- package/src/list-fast-path.test.ts +83 -0
- package/src/list-reactive-item.test.ts +77 -0
- package/src/ops.ts +419 -60
- package/src/props.test.ts +61 -1
- package/src/ref-cleanup.test.ts +144 -0
- package/src/ref.ts +57 -8
- package/src/remove-listener-identity.test.ts +97 -0
- package/src/renderer.ts +3 -1
- package/src/select-hidden-subscription.test.ts +104 -0
- package/src/show-classlist.test.ts +205 -0
- package/src/show.ts +107 -0
- package/src/spread-stable-shape.test.ts +109 -0
- package/src/spread.test.ts +86 -0
- package/src/svg.test.ts +28 -1
- package/src/svg.ts +34 -0
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Implement `VobsRenderer` (node creation, insertion, removal, attribute/property/
|
|
|
69
69
|
| `insertList(parent, anchor, factory, options?)` | Keyed/indexed list reconciliation with per-row owners. |
|
|
70
70
|
| `createFragment(render)` | Multi-root node container. |
|
|
71
71
|
| `ErrorBoundary` / `insertErrorBoundary` | Catches child render/effect errors with retry. |
|
|
72
|
-
| `insertBoundary` | Low-level boundary primitive (
|
|
72
|
+
| `insertBoundary` | Low-level boundary primitive shared by the error/async/resource boundaries (`children` / `fallback` / `onRetry` / `resetKey`). |
|
|
73
73
|
| `AsyncBoundary` / `insertAsyncBoundary` | Async view swapping with fallback. |
|
|
74
74
|
| `Profiler` / `insertProfiler` | Render timing instrumentation. |
|
|
75
75
|
| HMR exports (`createHmrStateStore`, `registerHmrInstance`, `updateHmrModule`, ...) | Hot-reload state preservation. |
|
package/dist/async-boundary.cjs
CHANGED
|
@@ -188,7 +188,7 @@ function syncSelectValue(parent) {
|
|
|
188
188
|
while (current !== null) {
|
|
189
189
|
if (current.nodeName === "SELECT") {
|
|
190
190
|
const read = selectValueReaders.get(current);
|
|
191
|
-
if (read !== void 0) setProperty(current, "value",
|
|
191
|
+
if (read !== void 0) setProperty(current, "value", (0, import_reactivity3.untrack)(read));
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
current = current.parentNode;
|
|
@@ -196,9 +196,9 @@ function syncSelectValue(parent) {
|
|
|
196
196
|
}
|
|
197
197
|
__name(syncSelectValue, "syncSelectValue");
|
|
198
198
|
function removeChild(parent, child) {
|
|
199
|
-
disposeNodeOwner(child);
|
|
200
199
|
if (isVobsFragment(child)) {
|
|
201
200
|
child.unmount(parent);
|
|
201
|
+
disposeNodeOwner(child);
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
getRenderer().removeChild(parent, child);
|
|
@@ -209,11 +209,12 @@ function removeChild(parent, child) {
|
|
|
209
209
|
parent: describeDebugNode(parent)
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
|
+
disposeNodeOwner(child);
|
|
212
213
|
}
|
|
213
214
|
__name(removeChild, "removeChild");
|
|
214
215
|
function setProperty(node, key, value) {
|
|
215
216
|
const previousValue = getRuntimeDebugHooks() ? readDebugValue(() => Reflect.get(node, key)) : void 0;
|
|
216
|
-
getRenderer().setProperty(node, key, value);
|
|
217
|
+
getRenderer().setProperty(node, PROPERTY_IDL_NAMES[key] ?? key, value);
|
|
217
218
|
if (key === "value" && node.tagName === "SELECT") {
|
|
218
219
|
scheduleSelectValueSync(node, value);
|
|
219
220
|
}
|
|
@@ -234,15 +235,19 @@ function scheduleSelectValueSync(node, value) {
|
|
|
234
235
|
pendingSelectValues.set(node, value);
|
|
235
236
|
if (selectSyncScheduled.has(node)) return;
|
|
236
237
|
selectSyncScheduled.add(node);
|
|
238
|
+
const renderer = getRenderer();
|
|
237
239
|
queueMicrotask(() => {
|
|
238
240
|
selectSyncScheduled.delete(node);
|
|
239
241
|
if (!pendingSelectValues.has(node)) return;
|
|
240
242
|
const pending = pendingSelectValues.get(node);
|
|
241
243
|
pendingSelectValues.delete(node);
|
|
242
|
-
|
|
244
|
+
renderer.setProperty(node, "value", pending);
|
|
243
245
|
});
|
|
244
246
|
}
|
|
245
247
|
__name(scheduleSelectValueSync, "scheduleSelectValueSync");
|
|
248
|
+
var PROPERTY_IDL_NAMES = {
|
|
249
|
+
autoFocus: "autofocus"
|
|
250
|
+
};
|
|
246
251
|
function createBlock(factory) {
|
|
247
252
|
const owner = (0, import_reactivity3.createOwner)();
|
|
248
253
|
(0, import_reactivity3.setOwnerDebugName)(owner, "dynamic");
|
|
@@ -262,6 +267,11 @@ function createBlock(factory) {
|
|
|
262
267
|
}
|
|
263
268
|
__name(createBlock, "createBlock");
|
|
264
269
|
function associateNodeOwner(node, owner) {
|
|
270
|
+
if (node === null || node === void 0 || typeof node !== "object" && typeof node !== "function") {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Vobs: associateNodeOwner \u6536\u5230\u975E\u8282\u70B9\u503C\uFF08${String(node)}\uFF09\u3002\u7EC4\u4EF6\u5FC5\u987B\u8FD4\u56DE VobsNode\uFF1B\u8FD4\u56DE null/undefined \u7531 createComponent \u8F6C\u6210\u7A7A\u6CE8\u91CA\u8282\u70B9\u3002`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
265
275
|
nodeOwners.set(node, owner);
|
|
266
276
|
}
|
|
267
277
|
__name(associateNodeOwner, "associateNodeOwner");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/async-boundary.ts","../src/dynamic.ts","../src/ops.ts","../src/fragment.ts","../src/debug.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { createOwner, state } from '@vobs/reactivity'\nimport { insertDynamic, type NodeFactory } from './dynamic'\nimport { createFragment, type VobsNode } from './fragment'\n\nexport type AsyncBoundaryView = ReturnType<NodeFactory> | NodeFactory\nexport type AsyncBoundaryFallback = (error: Error, retry: () => void) => ReturnType<NodeFactory>\nexport interface AsyncBoundaryOptions<T> {\n promise: PromiseLike<T> | (() => PromiseLike<T>)\n children: (value: T) => ReturnType<NodeFactory>\n loading?: AsyncBoundaryView\n fallback?: AsyncBoundaryFallback\n resetKey?: () => unknown\n}\nexport interface AsyncBoundaryProps<T> extends AsyncBoundaryOptions<T> {}\n\nexport function insertAsyncBoundary<T>(parent: Node, anchor: Node | null, options: AsyncBoundaryOptions<T>): void {\n const boundary = createOwner()\n boundary.run(() => {\n const loading = state(true)\n const data = state<T | undefined>(undefined)\n const resolved = state(false)\n const error = state<Error | null>(null)\n let token = 0\n let previousKey: unknown\n let initialized = false\n let fallbackActive = false\n boundary.onError(reason => {\n if (fallbackActive) throw reason\n error.value = normalizeError(reason)\n loading.value = false\n })\n\n const start = (): void => {\n const current = ++token\n loading.value = true\n error.value = null\n data.value = undefined\n resolved.value = false\n let promise: PromiseLike<T>\n try {\n promise = typeof options.promise === 'function' ? options.promise() : options.promise\n } catch (reason) {\n if (current === token) {\n loading.value = false\n error.value = normalizeError(reason)\n }\n return\n }\n Promise.resolve(promise).then(value => {\n if (current !== token || boundary.disposed) return\n data.value = value\n resolved.value = true\n loading.value = false\n }, reason => {\n if (current !== token || boundary.disposed) return\n error.value = normalizeError(reason)\n loading.value = false\n })\n }\n\n const retry = (): void => start()\n start()\n insertDynamic(parent, anchor, () => {\n const key = options.resetKey?.()\n if (!initialized) {\n initialized = true\n previousKey = key\n } else if (!Object.is(previousKey, key)) {\n previousKey = key\n start()\n }\n if (loading.value) {\n fallbackActive = false\n return resolveView(options.loading)\n }\n if (error.value) {\n fallbackActive = true\n return options.fallback?.(error.value, retry) ?? null\n }\n fallbackActive = false\n return resolved.value ? options.children(data.value as T) : resolveView(options.loading)\n })\n })\n}\n\nexport function AsyncBoundary<T>(props: AsyncBoundaryProps<T>): VobsNode {\n return createFragment((parent, anchor) => insertAsyncBoundary(parent, anchor, props))\n}\n\nfunction resolveView(view: AsyncBoundaryView | undefined): ReturnType<NodeFactory> {\n if (!view) return null\n return typeof view === 'function' ? view() : view\n}\n\nfunction normalizeError(reason: unknown): Error {\n return reason instanceof Error ? reason : new Error(String(reason))\n}\n","import { createOwner, effect, state, type Owner, type Signal } from '@vobs/reactivity'\nimport {\n associateNodeOwner,\n createBlock,\n createComment,\n createText,\n insertBefore,\n removeChild,\n setTextContent\n} from './ops'\nimport { createFragment, isVobsFragment, type VobsNode } from './fragment'\n\nexport type NodeFactory = () => VobsNode | null | undefined | false\nexport type DynamicChild = VobsNode | string | number | boolean | null | undefined | readonly DynamicChild[]\n\ninterface ListEntry<T> {\n key: unknown\n node: VobsNode\n owner: Owner\n viewOwner: Owner\n item: Signal<T>\n /** 最近一次赋值的原始 item:供比较与刷新使用,避免在列表 effect 内读取 item 信号造成自依赖。 */\n value: T\n index: number\n}\n\nexport function insertDynamic(parent: Node, anchor: Node | null, factory: NodeFactory): void {\n const marker = createComment('vobs:dynamic')\n insertBefore(parent, marker, anchor)\n let current: VobsNode | null = null\n\n effect(() => {\n const next = createBlock(factory)\n if (next === current) return\n\n if (current) removeChild(parent, current)\n current = next\n if (current) insertBefore(parent, current, marker)\n })\n}\n\n/**\n * Inserts any Vobs child value. This is the escape hatch for JSX expressions\n * that return a node, an array of nodes, text, or an empty conditional value.\n *\n * 多态插入(Solid 式):编译器把所有非静态子表达式统一路由到这里,值类型在运行时\n * 分发——原始值走自管理文本节点的原地更新快路径(等价 bindText 性能),节点/Fragment/\n * 数组走 scope 隔离的挂载/卸载路径。函数调用返回节点因此与返回字符串同样安全。\n */\nexport function insertDynamicValue(\n parent: Node,\n anchor: Node | null,\n factory: () => DynamicChild\n): void {\n const marker = createComment('vobs:value')\n insertBefore(parent, marker, anchor)\n let current: VobsNode | null = null\n /** current 是否为本 effect 自建的文本节点(可安全原地改写内容) */\n let currentIsText = false\n let scope: Owner | null = null\n\n effect(() => {\n // 每轮求值使用独立 scope Owner:factory 求值与挂载期间创建的组件 Owner 全部挂在它下面。\n // 数组/节点被替换时整体 dispose 旧 scope,避免被丢弃子树的 effect 继续订阅信号(幽灵更新与内存泄漏)。\n const nextScope = createOwner()\n const value = nextScope.run(factory)\n // ── 原始值快路径:自建文本节点原地改写,不销毁重建(高频文本零 GC)──\n if (typeof value === 'string' || typeof value === 'number') {\n if (currentIsText) {\n setTextContent(current as Text, String(value))\n nextScope.dispose()\n return\n }\n if (current) removeChild(parent, current)\n scope?.dispose()\n scope = null\n const text = createText(String(value))\n insertBefore(parent, text, marker)\n current = text\n currentIsText = true\n nextScope.dispose()\n return\n }\n if (value === null || value === undefined || typeof value === 'boolean') {\n if (current) removeChild(parent, current)\n scope?.dispose()\n scope = null\n current = null\n currentIsText = false\n nextScope.dispose()\n return\n }\n // ── 节点/Fragment/数组路径:scope 隔离的挂载与整体替换 ──\n const next = nextScope.run(() => normalizeDynamicChild(value))\n if (next === current) {\n nextScope.dispose()\n return\n }\n if (current) removeChild(parent, current)\n scope?.dispose()\n scope = nextScope\n current = next\n currentIsText = false\n if (current) insertBefore(parent, current, marker)\n })\n}\n\n/** Convert a JSX child value to a host node without creating a wrapper element. */\nexport function normalizeDynamicChild(value: DynamicChild): VobsNode | null {\n if (value === null || value === undefined || typeof value === 'boolean') return null\n if (typeof value === 'string' || typeof value === 'number') return createText(String(value))\n if (isVobsFragment(value) || isHostNode(value)) return value as VobsNode\n if (Array.isArray(value)) {\n const children = value\n if (children.length === 0) return null\n return createFragment((parent, anchor) => {\n for (const child of children) {\n const node = normalizeDynamicChild(child)\n if (node) insertBefore(parent, node, anchor)\n }\n })\n }\n return null\n}\n\nfunction isHostNode(value: unknown): value is Node {\n return Boolean(value && typeof value === 'object'\n && ('nodeType' in value || ((value as { type?: unknown }).type === 'element'\n || (value as { type?: unknown }).type === 'text'\n || (value as { type?: unknown }).type === 'comment')))\n}\n\nexport function insertList<T>(\n parent: Node,\n anchor: Node | null,\n source: () => readonly T[],\n renderItem: (item: T, index: number) => VobsNode,\n keyOf?: (item: T, index: number) => unknown\n): void {\n const marker = createComment('vobs:list')\n insertBefore(parent, marker, anchor)\n let entries: Array<ListEntry<T>> = []\n const tracksIndex = renderItem.length >= 2\n\n effect(() => {\n const items = source()\n\n // key 只计算一次:keyed 判定与调和共用,避免 keyOf 每轮被调用两遍。\n let keys: unknown[] | null = null\n if (keyOf) {\n keys = new Array(items.length)\n let allKeyed = items.length > 0\n for (let index = 0; index < items.length; index++) {\n const key = keyOf(items[index], index)\n if (key == null) {\n allKeyed = false\n break\n }\n keys[index] = key\n }\n if (!allKeyed) keys = null\n }\n\n const nextEntries = keys\n ? reconcileKeyed(items, keys, entries, renderItem)\n : reconcileIndexed(items, entries, renderItem)\n\n // tracksIndex 时 index 参与渲染,位置变化的行必须整体重建,新节点不在 DOM 中,\n // 重排阶段强制插入;否则只同步记录的位置,节点保持原样交给重排阶段移动。\n const refreshed = tracksIndex ? new Set<ListEntry<T>>() : null\n for (let index = 0; index < nextEntries.length; index++) {\n const entry = nextEntries[index]\n if (tracksIndex) {\n if (entry.index !== index) {\n refreshListEntry(parent, entry, index, renderItem)\n refreshed!.add(entry)\n }\n } else {\n entry.index = index\n }\n }\n\n const retained = new Set(nextEntries)\n for (const entry of entries) {\n if (!retained.has(entry)) disposeEntry(parent, entry)\n }\n\n reorderListEntries(parent, marker, entries, nextEntries, refreshed)\n entries = nextEntries\n })\n}\n\n/**\n * 按 nextEntries 顺序整理 DOM,但只移动必须移动的节点。\n */\nfunction reorderListEntries<T>(\n parent: Node,\n marker: Node,\n previous: readonly ListEntry<T>[],\n nextEntries: readonly ListEntry<T>[],\n forceInsert: Set<ListEntry<T>> | null\n): void {\n const count = nextEntries.length\n if (count === 0) return\n\n // 首次挂载(旧列表为空):全部是新节点,倒序直插即可,跳过 Map/LIS 构建。\n if (previous.length === 0) {\n let reference: VobsNode = marker\n for (let index = count - 1; index >= 0; index--) {\n const node = nextEntries[index].node\n insertBefore(parent, node, reference)\n reference = node\n }\n return\n }\n\n const oldIndexOf = new Map<ListEntry<T>, number>()\n for (let index = 0; index < previous.length; index++) oldIndexOf.set(previous[index], index)\n\n // seq[i] = 条目在旧序中的位置;新条目、重建条目与强制插入条目为 -1。\n const seq: number[] = new Array(count)\n for (let index = 0; index < count; index++) {\n const entry = nextEntries[index]\n seq[index] = forceInsert?.has(entry) ? -1 : oldIndexOf.get(entry) ?? -1\n }\n\n const keep = computeKeptByLis(seq)\n\n let reference: VobsNode = marker\n for (let index = count - 1; index >= 0; index--) {\n const node = nextEntries[index].node\n if (keep[index]) {\n reference = node\n continue\n }\n insertBefore(parent, node, reference)\n reference = node\n }\n}\n\n/**\n * 严格递增子序列(LIS)成员标记,O(n log n)。\n * 负值(新节点)不参与 LIS,永远视为需要移动。\n */\nfunction computeKeptByLis(seq: readonly number[]): boolean[] {\n const count = seq.length\n const keep = new Array<boolean>(count).fill(false)\n const tailsIndex: number[] = []\n const tailsValue: number[] = []\n const prev = new Array<number>(count).fill(-1)\n\n for (let i = 0; i < count; i++) {\n const value = seq[i]\n if (value < 0) continue\n let lo = 0\n let hi = tailsValue.length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (tailsValue[mid] < value) lo = mid + 1\n else hi = mid\n }\n if (lo === tailsValue.length) {\n tailsValue.push(value)\n tailsIndex.push(i)\n } else {\n tailsValue[lo] = value\n tailsIndex[lo] = i\n }\n prev[i] = lo > 0 ? tailsIndex[lo - 1] : -1\n }\n\n let cursor = tailsIndex.length > 0 ? tailsIndex[tailsIndex.length - 1] : -1\n while (cursor >= 0) {\n keep[cursor] = true\n cursor = prev[cursor]\n }\n return keep\n}\n\nfunction reconcileKeyed<T>(\n items: readonly T[],\n keys: readonly unknown[],\n entries: Array<ListEntry<T>>,\n renderItem: (item: T, index: number) => VobsNode\n): Array<ListEntry<T>> {\n const previous = new Map(entries.map(entry => [entry.key, entry]))\n const seen = new Set<unknown>()\n const nextEntries: Array<ListEntry<T>> = []\n\n for (let index = 0; index < items.length; index++) {\n const item = items[index]\n const key = keys[index]\n if (seen.has(key)) {\n console.warn(`Vobs: 检测到重复的列表 key: ${String(key)}`)\n }\n seen.add(key)\n const entry = previous.get(key)\n if (entry) {\n previous.delete(key)\n if (isPrimitiveItem(item) && !Object.is(entry.value, item)) {\n // 原始类型项在编译产物中被静态捕获,无法通过 item 信号刷新视图:值变化时必须重建行。\n nextEntries.push(createListEntry(item, index, key, renderItem))\n continue\n }\n entry.item.value = item\n entry.value = item\n nextEntries.push(entry)\n continue\n }\n nextEntries.push(createListEntry(item, index, key, renderItem))\n }\n\n return nextEntries\n}\n\nfunction reconcileIndexed<T>(\n items: readonly T[],\n entries: Array<ListEntry<T>>,\n renderItem: (item: T, index: number) => VobsNode\n): Array<ListEntry<T>> {\n const nextEntries: Array<ListEntry<T>> = []\n for (let index = 0; index < items.length; index++) {\n const item = items[index]\n const entry = entries[index]\n if (entry && isPrimitiveItem(item) && !Object.is(entry.value, item)) {\n // 原始类型项被编译产物静态捕获,值变化时必须重建行;旧行由主循环统一 dispose。\n nextEntries.push(createListEntry(item, index, index, renderItem))\n continue\n }\n if (entry) {\n entry.item.value = item\n entry.value = item\n nextEntries.push(entry)\n continue\n }\n nextEntries.push(createListEntry(item, index, index, renderItem))\n }\n return nextEntries\n}\n\n/** 原始类型(含 function)无法被 Proxy 响应化,只能整行重建。 */\nfunction isPrimitiveItem(item: unknown): boolean {\n return item === null || typeof item !== 'object'\n}\n\nfunction createListEntry<T>(\n item: T,\n index: number,\n key: unknown,\n renderItem: (item: T, index: number) => VobsNode\n): ListEntry<T> {\n const owner = createOwner()\n let itemSignal!: Signal<T>\n let viewOwner!: Owner\n let node!: VobsNode\n owner.run(() => {\n itemSignal = state(item)\n viewOwner = createOwner()\n node = viewOwner.run(() => renderItem(toReactiveItem(itemSignal, item), index))\n })\n associateNodeOwner(node, viewOwner)\n return { key, node, owner, viewOwner, item: itemSignal, value: item, index }\n}\n\nfunction refreshListEntry<T>(\n parent: Node,\n entry: ListEntry<T>,\n index: number,\n renderItem: (item: T, index: number) => VobsNode\n): void {\n removeChild(parent, entry.node)\n entry.index = index\n const previousView = entry.viewOwner\n entry.owner.run(() => {\n entry.viewOwner = createOwner()\n entry.node = entry.viewOwner.run(() => renderItem(\n toReactiveItem(entry.item, entry.value),\n index\n ))\n })\n // 旧视图的 effect 仍订阅着 item 信号,必须 dispose,否则会写已脱离的 DOM(幽灵更新)。\n previousView.dispose()\n associateNodeOwner(entry.node, entry.viewOwner)\n}\n\nfunction toReactiveItem<T>(item: Signal<T>, initialValue: T): T {\n if (typeof initialValue !== 'object' || initialValue === null) {\n return initialValue\n }\n\n // 代理目标使用创建时的原始对象:避免在列表 effect 内读取 item 信号造成自依赖;\n // 属性读取始终转发到 item.value,在行内绑定 effect 中被正常追踪。\n return new Proxy(initialValue as object, {\n get(_target, property, receiver) {\n return Reflect.get(item.value as object, property, receiver)\n },\n has(_target, property) {\n return property in (item.value as object)\n },\n ownKeys() {\n return Reflect.ownKeys(item.value as object)\n },\n getOwnPropertyDescriptor(_target, property) {\n return Object.getOwnPropertyDescriptor(item.value as object, property)\n }\n }) as T\n}\n\nfunction disposeEntry(parent: Node, entry: ListEntry<unknown>): void {\n removeChild(parent, entry.node)\n entry.owner.dispose()\n}\n","// 编译产物调用的基础操作\n\nimport { createOwner, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'\nimport { isVobsFragment, type VobsNode } from './fragment'\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\nexport function setRenderer<\n NodeType,\n TextNode extends NodeType,\n ElementNode extends NodeType,\n CommentNode extends NodeType\n>(renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode>): void {\n // 编译产物仍使用 DOM 节点声明;实际宿主类型由应用提供的渲染器决定。\n currentRenderer = renderer as unknown as RuntimeRenderer\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 命名空间(与 HTML 解析器 foreign-content 规则一致) */\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 */\nconst SVG_TAGS = 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\nexport function createElement(tag: string): Element {\n // SVG 标签走命名空间创建:document.createElement('rect') 产物是 HTMLUnknownElement,\n // 整棵 SVG 子树都不会渲染(无报错)。渲染器未实现 createSvgElement 时按 createElement\n // 兜底(旧自定义渲染器保持原行为)。\n if (SVG_TAGS.has(tag)) {\n const renderer = getRenderer()\n if (renderer.createSvgElement) return renderer.createSvgElement(tag)\n }\n return getRenderer().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 if (read !== undefined) setProperty(current as Element, 'value', read())\n return\n }\n current = current.parentNode\n }\n}\n\nexport function removeChild(\n parent: Node,\n child: VobsNode\n): void {\n disposeNodeOwner(child)\n if (isVobsFragment(child)) {\n child.unmount(parent)\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}\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, 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 queueMicrotask(() => {\n selectSyncScheduled.delete(node)\n if (!pendingSelectValues.has(node)) return\n const pending = pendingSelectValues.get(node)\n pendingSelectValues.delete(node)\n getRenderer().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 for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || value === null || value === undefined) continue\n if (key === 'ref') {\n setRef(node, value)\n continue\n }\n if (key.startsWith('on') && typeof value === 'function') addEventListener(node, key.slice(2).toLowerCase(), value as EventListener)\n // property 键的 false 有语义(如 disabled={false} 必须清除),不能跳过;\n // attribute 键的 false 表示“不设置”,与 HTML 语义一致。\n else if (isPropertyKey(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/** 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 (isPropertyKey(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\nfunction isPropertyKey(key: string): boolean {\n return key === 'value' || key === 'checked' || key === 'selected' || key === 'disabled'\n || key === 'multiple' || key === 'readOnly' || key === 'required'\n || key === 'autofocus' || key === 'hidden' || key === 'tabIndex'\n || key === 'colSpan' || key === 'rowSpan'\n}\n\n/** camelCase JSX 属性名 → HTML attribute 名(与 compiler 的 domAttributeName 保持一致) */\nfunction domAttributeName(key: string): string {\n switch (key) {\n case 'className': return 'class'\n case 'htmlFor': return 'for'\n case 'autoComplete': return 'autocomplete'\n case 'spellCheck': return 'spellcheck'\n default: return key\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) renderer.removeEventListener(node, event, previous.handler)\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 if (!handled) throw error\n }\n } : handler\n const binding: EventBinding = { handler: listener, owner, original: handler }\n bindings.set(event, binding)\n renderer.addEventListener(node, event, listener)\n owner?.onDispose(() => {\n if (bindings?.get(event) !== binding) return\n bindings.delete(event)\n renderer.removeEventListener(node, event, listener)\n })\n}\n\nexport function removeEventListener(\n node: Element,\n event: string,\n handler: EventListener\n): void {\n const renderer = getRenderer()\n const binding = eventBindings.get(node)?.get(event)\n renderer.removeEventListener(node, event, binding?.handler ?? handler)\n eventBindings.get(node)?.delete(event)\n}\n\nexport function clear(container: Node): void {\n getRenderer().clear(container)\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 let node: VobsNode\n try {\n // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值\n // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,\n // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发\n // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。\n // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。\n node = 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 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\nexport function associateNodeOwner(node: VobsNode, owner: Owner): void {\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 { 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","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'\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\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'\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 {\n // HMR failures remain application errors on the next normal render.\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 } from '@vobs/reactivity'\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/** 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 owner?.onDispose(() => {\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 })\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 {\n // Ref callbacks are user code; never make mounting fail because of them.\n }\n}\n"],"mappings":"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","names":["import_reactivity","import_reactivity","import_reactivity","import_reactivity"]}
|
|
1
|
+
{"version":3,"sources":["../src/async-boundary.ts","../src/dynamic.ts","../src/ops.ts","../src/fragment.ts","../src/debug.ts","../src/hmr.ts","../src/ref.ts"],"sourcesContent":["import { createOwner, state } from '@vobs/reactivity'\nimport { insertDynamic, type NodeFactory } from './dynamic'\nimport { createFragment, type VobsNode } from './fragment'\n\nexport type AsyncBoundaryView = ReturnType<NodeFactory> | NodeFactory\nexport type AsyncBoundaryFallback = (error: Error, retry: () => void) => ReturnType<NodeFactory>\nexport interface AsyncBoundaryOptions<T> {\n promise: PromiseLike<T> | (() => PromiseLike<T>)\n children: (value: T) => ReturnType<NodeFactory>\n loading?: AsyncBoundaryView\n fallback?: AsyncBoundaryFallback\n resetKey?: () => unknown\n}\nexport interface AsyncBoundaryProps<T> extends AsyncBoundaryOptions<T> {}\n\nexport function insertAsyncBoundary<T>(parent: Node, anchor: Node | null, options: AsyncBoundaryOptions<T>): void {\n const boundary = createOwner()\n boundary.run(() => {\n const loading = state(true)\n const data = state<T | undefined>(undefined)\n const resolved = state(false)\n const error = state<Error | null>(null)\n let token = 0\n let previousKey: unknown\n let initialized = false\n let fallbackActive = false\n boundary.onError(reason => {\n if (fallbackActive) throw reason\n error.value = normalizeError(reason)\n loading.value = false\n })\n\n const start = (): void => {\n const current = ++token\n loading.value = true\n error.value = null\n data.value = undefined\n resolved.value = false\n let promise: PromiseLike<T>\n try {\n promise = typeof options.promise === 'function' ? options.promise() : options.promise\n } catch (reason) {\n if (current === token) {\n loading.value = false\n error.value = normalizeError(reason)\n }\n return\n }\n Promise.resolve(promise).then(value => {\n if (current !== token || boundary.disposed) return\n data.value = value\n resolved.value = true\n loading.value = false\n }, reason => {\n if (current !== token || boundary.disposed) return\n error.value = normalizeError(reason)\n loading.value = false\n })\n }\n\n const retry = (): void => start()\n start()\n insertDynamic(parent, anchor, () => {\n const key = options.resetKey?.()\n if (!initialized) {\n initialized = true\n previousKey = key\n } else if (!Object.is(previousKey, key)) {\n previousKey = key\n start()\n }\n if (loading.value) {\n fallbackActive = false\n return resolveView(options.loading)\n }\n if (error.value) {\n fallbackActive = true\n return options.fallback?.(error.value, retry) ?? null\n }\n fallbackActive = false\n return resolved.value ? options.children(data.value as T) : resolveView(options.loading)\n })\n })\n}\n\nexport function AsyncBoundary<T>(props: AsyncBoundaryProps<T>): VobsNode {\n return createFragment((parent, anchor) => insertAsyncBoundary(parent, anchor, props))\n}\n\nfunction resolveView(view: AsyncBoundaryView | undefined): ReturnType<NodeFactory> {\n if (!view) return null\n return typeof view === 'function' ? view() : view\n}\n\nfunction normalizeError(reason: unknown): Error {\n return reason instanceof Error ? reason : new Error(String(reason))\n}\n","import { createOwner, effect, state, type Owner, type Signal } from '@vobs/reactivity'\nimport {\n associateNodeOwner,\n createBlock,\n createComment,\n createText,\n insertBefore,\n removeChild,\n setTextContent\n} from './ops'\nimport { createFragment, isVobsFragment, type VobsNode } from './fragment'\n\nexport type NodeFactory = () => VobsNode | null | undefined | false\nexport type DynamicChild = VobsNode | string | number | boolean | null | undefined | readonly DynamicChild[]\n\ninterface ListEntry<T> {\n key: unknown\n node: VobsNode\n owner: Owner\n viewOwner: Owner\n item: Signal<T>\n /** 最近一次赋值的原始 item:供比较与刷新使用,避免在列表 effect 内读取 item 信号造成自依赖。\n * (刻意不叫 value —— `vobs check` 的 VOBS_C210 会把 `X.value` 的读写当成信号自订阅)。 */\n current: T\n index: number\n}\n\nexport function insertDynamic(parent: Node, anchor: Node | null, factory: NodeFactory): void {\n const marker = createComment('vobs:dynamic')\n insertBefore(parent, marker, anchor)\n let current: VobsNode | null = null\n\n effect(() => {\n const next = createBlock(factory)\n if (next === current) return\n\n if (current) removeChild(parent, current)\n current = next\n if (current) insertBefore(parent, current, marker)\n })\n}\n\n/**\n * Inserts any Vobs child value. This is the escape hatch for JSX expressions\n * that return a node, an array of nodes, text, or an empty conditional value.\n *\n * 多态插入(Solid 式):编译器把所有非静态子表达式统一路由到这里,值类型在运行时\n * 分发——原始值走自管理文本节点的原地更新快路径(等价 bindText 性能),节点/Fragment/\n * 数组走 scope 隔离的挂载/卸载路径。函数调用返回节点因此与返回字符串同样安全。\n */\nexport function insertDynamicValue(\n parent: Node,\n anchor: Node | null,\n factory: () => DynamicChild\n): void {\n const marker = createComment('vobs:value')\n insertBefore(parent, marker, anchor)\n let current: VobsNode | null = null\n /** current 是否为本 effect 自建的文本节点(可安全原地改写内容) */\n let currentIsText = false\n let scope: Owner | null = null\n\n effect(() => {\n // 每轮求值使用独立 scope Owner:factory 求值与挂载期间创建的组件 Owner 全部挂在它下面。\n // 数组/节点被替换时整体 dispose 旧 scope,避免被丢弃子树的 effect 继续订阅信号(幽灵更新与内存泄漏)。\n const nextScope = createOwner()\n const value = nextScope.run(factory)\n // ── 原始值快路径:自建文本节点原地改写,不销毁重建(高频文本零 GC)──\n if (typeof value === 'string' || typeof value === 'number') {\n if (currentIsText) {\n setTextContent(current as Text, String(value))\n nextScope.dispose()\n return\n }\n if (current) removeChild(parent, current)\n scope?.dispose()\n scope = null\n const text = createText(String(value))\n insertBefore(parent, text, marker)\n current = text\n currentIsText = true\n nextScope.dispose()\n return\n }\n if (value === null || value === undefined || typeof value === 'boolean') {\n if (current) removeChild(parent, current)\n scope?.dispose()\n scope = null\n current = null\n currentIsText = false\n nextScope.dispose()\n return\n }\n // ── 节点/Fragment/数组路径:scope 隔离的挂载与整体替换 ──\n const next = nextScope.run(() => normalizeDynamicChild(value))\n if (next === current) {\n nextScope.dispose()\n return\n }\n if (current) removeChild(parent, current)\n scope?.dispose()\n scope = nextScope\n current = next\n currentIsText = false\n if (current) insertBefore(parent, current, marker)\n })\n}\n\n/** Convert a JSX child value to a host node without creating a wrapper element. */\nexport function normalizeDynamicChild(value: DynamicChild): VobsNode | null {\n if (value === null || value === undefined || typeof value === 'boolean') return null\n if (typeof value === 'string' || typeof value === 'number') return createText(String(value))\n if (isVobsFragment(value) || isHostNode(value)) return value as VobsNode\n if (Array.isArray(value)) {\n const children = value\n if (children.length === 0) return null\n return createFragment((parent, anchor) => {\n for (const child of children) {\n const node = normalizeDynamicChild(child)\n if (node) insertBefore(parent, node, anchor)\n }\n })\n }\n return null\n}\n\nfunction isHostNode(value: unknown): value is Node {\n return Boolean(value && typeof value === 'object'\n && ('nodeType' in value || ((value as { type?: unknown }).type === 'element'\n || (value as { type?: unknown }).type === 'text'\n || (value as { type?: unknown }).type === 'comment')))\n}\n\nexport function insertList<T>(\n parent: Node,\n anchor: Node | null,\n source: () => readonly T[],\n renderItem: (item: T, index: number) => VobsNode,\n keyOf?: (item: T, index: number) => unknown\n): void {\n const marker = createComment('vobs:list')\n insertBefore(parent, marker, anchor)\n let entries: Array<ListEntry<T>> = []\n const tracksIndex = renderItem.length >= 2\n\n effect(() => {\n const items = source()\n\n // key 只计算一次:keyed 判定与调和共用,避免 keyOf 每轮被调用两遍。\n let keys: unknown[] | null = null\n if (keyOf) {\n keys = new Array(items.length)\n let allKeyed = items.length > 0\n for (let index = 0; index < items.length; index++) {\n const key = keyOf(items[index], index)\n if (key == null) {\n allKeyed = false\n break\n }\n keys[index] = key\n }\n if (!allKeyed) keys = null\n }\n\n /*\n * 快路径:key 序列与上一轮**逐位相同** —— 行一个不多、一个不少、顺序也没变。\n *\n * 此时 DOM 已经是正确顺序(每次更新末尾的重排保证了「DOM 顺序 === entries 顺序」),\n * 于是建表(Map)、查重(Set)、差集(retained Set)、重排(seq 数组 + LIS)**全都不需要**,\n * 只同步每行的 item 与 index。这些记账是逐行的,200 行一次的固定成本实测约 34µs,\n * 而这一轮真正需要做的 DOM 写只有 0.2µs(见 scripts/bench)。\n *\n * 扫描本身是 O(n) 的两次比较(key 与是否需重建),比省掉的记账便宜一个数量级。\n * 任何一行需要重建(原始类型项的值变了 —— 那种行的视图是静态捕获的)就退回常规路径。\n */\n if (keys !== null && entries.length === items.length) {\n let fast = true\n for (let index = 0; index < keys.length; index++) {\n if (!Object.is(entries[index].key, keys[index])) {\n fast = false\n break\n }\n const item = items[index]\n if (isPrimitiveItem(item) && !Object.is(entries[index].current, item)) {\n fast = false\n break\n }\n }\n if (fast) {\n for (let index = 0; index < items.length; index++) {\n const entry = entries[index]\n entry.index = index\n const item = items[index]\n if (!Object.is(entry.current, item)) {\n // item 信号写入在值相同时会被 Object.is 短路,所以这里只对真正变了的行动手\n entry.item.value = item\n entry.current = item\n }\n }\n return\n }\n }\n\n const nextEntries = keys\n ? reconcileKeyed(items, keys, entries, renderItem)\n : reconcileIndexed(items, entries, renderItem)\n\n // tracksIndex 时 index 参与渲染,位置变化的行必须整体重建,新节点不在 DOM 中,\n // 重排阶段强制插入;否则只同步记录的位置,节点保持原样交给重排阶段移动。\n const refreshed = tracksIndex ? new Set<ListEntry<T>>() : null\n for (let index = 0; index < nextEntries.length; index++) {\n const entry = nextEntries[index]\n if (tracksIndex) {\n if (entry.index !== index) {\n refreshListEntry(parent, entry, index, renderItem)\n refreshed!.add(entry)\n }\n } else {\n entry.index = index\n }\n }\n\n const retained = new Set(nextEntries)\n for (const entry of entries) {\n if (!retained.has(entry)) disposeEntry(parent, entry)\n }\n\n reorderListEntries(parent, marker, entries, nextEntries, refreshed)\n entries = nextEntries\n })\n}\n\n/**\n * 按 nextEntries 顺序整理 DOM,但只移动必须移动的节点。\n */\nfunction reorderListEntries<T>(\n parent: Node,\n marker: Node,\n previous: readonly ListEntry<T>[],\n nextEntries: readonly ListEntry<T>[],\n forceInsert: Set<ListEntry<T>> | null\n): void {\n const count = nextEntries.length\n if (count === 0) return\n\n // 首次挂载(旧列表为空):全部是新节点,倒序直插即可,跳过 Map/LIS 构建。\n if (previous.length === 0) {\n let reference: VobsNode = marker\n for (let index = count - 1; index >= 0; index--) {\n const node = nextEntries[index].node\n insertBefore(parent, node, reference)\n reference = node\n }\n return\n }\n\n const oldIndexOf = new Map<ListEntry<T>, number>()\n for (let index = 0; index < previous.length; index++) oldIndexOf.set(previous[index], index)\n\n // seq[i] = 条目在旧序中的位置;新条目、重建条目与强制插入条目为 -1。\n const seq: number[] = new Array(count)\n for (let index = 0; index < count; index++) {\n const entry = nextEntries[index]\n seq[index] = forceInsert?.has(entry) ? -1 : oldIndexOf.get(entry) ?? -1\n }\n\n const keep = computeKeptByLis(seq)\n\n let reference: VobsNode = marker\n for (let index = count - 1; index >= 0; index--) {\n const node = nextEntries[index].node\n if (keep[index]) {\n reference = node\n continue\n }\n insertBefore(parent, node, reference)\n reference = node\n }\n}\n\n/**\n * 严格递增子序列(LIS)成员标记,O(n log n)。\n * 负值(新节点)不参与 LIS,永远视为需要移动。\n */\nfunction computeKeptByLis(seq: readonly number[]): boolean[] {\n const count = seq.length\n const keep = new Array<boolean>(count).fill(false)\n const tailsIndex: number[] = []\n const tailsValue: number[] = []\n const prev = new Array<number>(count).fill(-1)\n\n for (let i = 0; i < count; i++) {\n const value = seq[i]\n if (value < 0) continue\n let lo = 0\n let hi = tailsValue.length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (tailsValue[mid] < value) lo = mid + 1\n else hi = mid\n }\n if (lo === tailsValue.length) {\n tailsValue.push(value)\n tailsIndex.push(i)\n } else {\n tailsValue[lo] = value\n tailsIndex[lo] = i\n }\n prev[i] = lo > 0 ? tailsIndex[lo - 1] : -1\n }\n\n let cursor = tailsIndex.length > 0 ? tailsIndex[tailsIndex.length - 1] : -1\n while (cursor >= 0) {\n keep[cursor] = true\n cursor = prev[cursor]\n }\n return keep\n}\n\nfunction reconcileKeyed<T>(\n items: readonly T[],\n keys: readonly unknown[],\n entries: Array<ListEntry<T>>,\n renderItem: (item: T, index: number) => VobsNode\n): Array<ListEntry<T>> {\n // 逐项建表,不用 entries.map(...):后者会先造一个等长的「键值对数组」中间产物,\n // 200 行就是 200 个元组对象的分配 —— 每次更新都白付一次。\n const previous = new Map<unknown, ListEntry<T>>()\n for (const entry of entries) previous.set(entry.key, entry)\n const seen = new Set<unknown>()\n const nextEntries: Array<ListEntry<T>> = []\n\n for (let index = 0; index < items.length; index++) {\n const item = items[index]\n const key = keys[index]\n if (seen.has(key)) {\n console.warn(`Vobs: 检测到重复的列表 key: ${String(key)}`)\n }\n seen.add(key)\n const entry = previous.get(key)\n if (entry) {\n previous.delete(key)\n if (isPrimitiveItem(item) && !Object.is(entry.current, item)) {\n // 原始类型项在编译产物中被静态捕获,无法通过 item 信号刷新视图:值变化时必须重建行。\n nextEntries.push(createListEntry(item, index, key, renderItem))\n continue\n }\n entry.item.value = item\n entry.current = item\n nextEntries.push(entry)\n continue\n }\n nextEntries.push(createListEntry(item, index, key, renderItem))\n }\n\n return nextEntries\n}\n\nfunction reconcileIndexed<T>(\n items: readonly T[],\n entries: Array<ListEntry<T>>,\n renderItem: (item: T, index: number) => VobsNode\n): Array<ListEntry<T>> {\n const nextEntries: Array<ListEntry<T>> = []\n for (let index = 0; index < items.length; index++) {\n const item = items[index]\n const entry = entries[index]\n if (entry && isPrimitiveItem(item) && !Object.is(entry.current, item)) {\n // 原始类型项被编译产物静态捕获,值变化时必须重建行;旧行由主循环统一 dispose。\n nextEntries.push(createListEntry(item, index, index, renderItem))\n continue\n }\n if (entry) {\n entry.item.value = item\n entry.current = item\n nextEntries.push(entry)\n continue\n }\n nextEntries.push(createListEntry(item, index, index, renderItem))\n }\n return nextEntries\n}\n\n/** 原始类型(含 function)无法被 Proxy 响应化,只能整行重建。 */\nfunction isPrimitiveItem(item: unknown): boolean {\n return item === null || typeof item !== 'object'\n}\n\nfunction createListEntry<T>(\n item: T,\n index: number,\n key: unknown,\n renderItem: (item: T, index: number) => VobsNode\n): ListEntry<T> {\n const owner = createOwner()\n let itemSignal!: Signal<T>\n let viewOwner!: Owner\n let node!: VobsNode\n owner.run(() => {\n itemSignal = state(item)\n viewOwner = createOwner()\n node = viewOwner.run(() => renderItem(toReactiveItem(itemSignal, item), index))\n })\n associateNodeOwner(node, viewOwner)\n return { key, node, owner, viewOwner, item: itemSignal, current: item, index }\n}\n\nfunction refreshListEntry<T>(\n parent: Node,\n entry: ListEntry<T>,\n index: number,\n renderItem: (item: T, index: number) => VobsNode\n): void {\n removeChild(parent, entry.node)\n entry.index = index\n const previousView = entry.viewOwner\n entry.owner.run(() => {\n entry.viewOwner = createOwner()\n entry.node = entry.viewOwner.run(() => renderItem(\n toReactiveItem(entry.item, entry.current),\n index\n ))\n })\n // 旧视图的 effect 仍订阅着 item 信号,必须 dispose,否则会写已脱离的 DOM(幽灵更新)。\n previousView.dispose()\n associateNodeOwner(entry.node, entry.viewOwner)\n}\n\nfunction toReactiveItem<T>(item: Signal<T>, initialValue: T): T {\n if (typeof initialValue !== 'object' || initialValue === null) {\n return initialValue\n }\n\n // 属性读取转发到 item.value(顶替后的新条目),由 get 陷阱完成追踪;目标见 reactiveProxyTarget。\n return new Proxy(reactiveProxyTarget(initialValue), {\n get(_target, property, receiver) {\n return Reflect.get(item.value as object, property, receiver)\n },\n has(_target, property) {\n return property in (item.value as object)\n },\n ownKeys() {\n return Reflect.ownKeys(item.value as object)\n },\n getOwnPropertyDescriptor(target, property) {\n const descriptor = Object.getOwnPropertyDescriptor(item.value as object, property)\n if (!descriptor) return undefined\n // 目标上已有的自有属性不能改报\"可配置性\"(数组的 length 就不可配置),否则违反不变量\n const own = Object.getOwnPropertyDescriptor(target, property)\n return { ...descriptor, configurable: own ? own.configurable : true }\n }\n }) as T\n}\n\n/**\n * 选择代理目标。\n *\n * 目标**不能**带\"不可配置\"的自有属性:代理把属性读取转发到 `item.value`(同 key 顶替后的\n * 新条目),而 Proxy 不变量要求 get / getOwnPropertyDescriptor / ownKeys 与目标一致。实测\n * `Object.freeze` 过的条目(@vobs/notification 每条通知都冻结)直接抛:\n *\n * TypeError: 'get' on proxy: property 'content' is a read-only and non-configurable data\n * property on the proxy target but the proxy did not return its actual value\n *\n * 于是\"冻结条目的行原地刷新\"这条路径根本不可能成立(首轮渲染恰好同值,把问题掩盖了)。\n *\n * 安全的条目(可扩展 + 自有属性全部可配置:普通字面量、DOM 包装器等)照旧用条目自身当目标 ——\n * jsdom 会把 sameObjectCaches 之类的 Symbol 缓存写在包装器上,写与读必须落在同一个对象上,\n * 换目标会让 `element.classList` 直接抛 TypeError。冻结/密封/defineProperty 过的条目改用\n * 同原型的空对象(数组用空数组,保住 Array.isArray)。\n */\nfunction reactiveProxyTarget(value: object): object {\n if (Object.isExtensible(value)) {\n let allConfigurable = true\n for (const key of Reflect.ownKeys(value)) {\n if (Object.getOwnPropertyDescriptor(value, key)?.configurable === false) {\n allConfigurable = false\n break\n }\n }\n if (allConfigurable) return value\n }\n return Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value) as object | null)\n}\n\nfunction disposeEntry(parent: Node, entry: ListEntry<unknown>): void {\n removeChild(parent, entry.node)\n entry.owner.dispose()\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 { 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","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":["import_reactivity","import_reactivity","import_reactivity","import_reactivity"]}
|
package/dist/async-boundary.js
CHANGED
|
@@ -5,10 +5,10 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
import { createOwner as createOwner3, state as state2 } from "@vobs/reactivity";
|
|
6
6
|
|
|
7
7
|
// packages/runtime/src/dynamic.ts
|
|
8
|
-
import { createOwner as createOwner2, effect, state } from "@vobs/reactivity";
|
|
8
|
+
import { createOwner as createOwner2, effect as effect2, state } from "@vobs/reactivity";
|
|
9
9
|
|
|
10
10
|
// packages/runtime/src/ops.ts
|
|
11
|
-
import { createOwner, getCurrentOwner as getCurrentOwner3, setOwnerDebugName, untrack } from "@vobs/reactivity";
|
|
11
|
+
import { createOwner, effect, getCurrentOwner as getCurrentOwner3, setOwnerDebugName, untrack } from "@vobs/reactivity";
|
|
12
12
|
|
|
13
13
|
// packages/runtime/src/fragment.ts
|
|
14
14
|
import { getCurrentOwner } from "@vobs/reactivity";
|
|
@@ -164,7 +164,7 @@ function syncSelectValue(parent) {
|
|
|
164
164
|
while (current !== null) {
|
|
165
165
|
if (current.nodeName === "SELECT") {
|
|
166
166
|
const read = selectValueReaders.get(current);
|
|
167
|
-
if (read !== void 0) setProperty(current, "value", read
|
|
167
|
+
if (read !== void 0) setProperty(current, "value", untrack(read));
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
current = current.parentNode;
|
|
@@ -172,9 +172,9 @@ function syncSelectValue(parent) {
|
|
|
172
172
|
}
|
|
173
173
|
__name(syncSelectValue, "syncSelectValue");
|
|
174
174
|
function removeChild(parent, child) {
|
|
175
|
-
disposeNodeOwner(child);
|
|
176
175
|
if (isVobsFragment(child)) {
|
|
177
176
|
child.unmount(parent);
|
|
177
|
+
disposeNodeOwner(child);
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
getRenderer().removeChild(parent, child);
|
|
@@ -185,11 +185,12 @@ function removeChild(parent, child) {
|
|
|
185
185
|
parent: describeDebugNode(parent)
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
+
disposeNodeOwner(child);
|
|
188
189
|
}
|
|
189
190
|
__name(removeChild, "removeChild");
|
|
190
191
|
function setProperty(node, key, value) {
|
|
191
192
|
const previousValue = getRuntimeDebugHooks() ? readDebugValue(() => Reflect.get(node, key)) : void 0;
|
|
192
|
-
getRenderer().setProperty(node, key, value);
|
|
193
|
+
getRenderer().setProperty(node, PROPERTY_IDL_NAMES[key] ?? key, value);
|
|
193
194
|
if (key === "value" && node.tagName === "SELECT") {
|
|
194
195
|
scheduleSelectValueSync(node, value);
|
|
195
196
|
}
|
|
@@ -210,15 +211,19 @@ function scheduleSelectValueSync(node, value) {
|
|
|
210
211
|
pendingSelectValues.set(node, value);
|
|
211
212
|
if (selectSyncScheduled.has(node)) return;
|
|
212
213
|
selectSyncScheduled.add(node);
|
|
214
|
+
const renderer = getRenderer();
|
|
213
215
|
queueMicrotask(() => {
|
|
214
216
|
selectSyncScheduled.delete(node);
|
|
215
217
|
if (!pendingSelectValues.has(node)) return;
|
|
216
218
|
const pending = pendingSelectValues.get(node);
|
|
217
219
|
pendingSelectValues.delete(node);
|
|
218
|
-
|
|
220
|
+
renderer.setProperty(node, "value", pending);
|
|
219
221
|
});
|
|
220
222
|
}
|
|
221
223
|
__name(scheduleSelectValueSync, "scheduleSelectValueSync");
|
|
224
|
+
var PROPERTY_IDL_NAMES = {
|
|
225
|
+
autoFocus: "autofocus"
|
|
226
|
+
};
|
|
222
227
|
function createBlock(factory) {
|
|
223
228
|
const owner = createOwner();
|
|
224
229
|
setOwnerDebugName(owner, "dynamic");
|
|
@@ -238,6 +243,11 @@ function createBlock(factory) {
|
|
|
238
243
|
}
|
|
239
244
|
__name(createBlock, "createBlock");
|
|
240
245
|
function associateNodeOwner(node, owner) {
|
|
246
|
+
if (node === null || node === void 0 || typeof node !== "object" && typeof node !== "function") {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Vobs: associateNodeOwner \u6536\u5230\u975E\u8282\u70B9\u503C\uFF08${String(node)}\uFF09\u3002\u7EC4\u4EF6\u5FC5\u987B\u8FD4\u56DE VobsNode\uFF1B\u8FD4\u56DE null/undefined \u7531 createComponent \u8F6C\u6210\u7A7A\u6CE8\u91CA\u8282\u70B9\u3002`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
241
251
|
nodeOwners.set(node, owner);
|
|
242
252
|
}
|
|
243
253
|
__name(associateNodeOwner, "associateNodeOwner");
|
|
@@ -254,7 +264,7 @@ function insertDynamic(parent, anchor, factory) {
|
|
|
254
264
|
const marker = createComment("vobs:dynamic");
|
|
255
265
|
insertBefore(parent, marker, anchor);
|
|
256
266
|
let current = null;
|
|
257
|
-
|
|
267
|
+
effect2(() => {
|
|
258
268
|
const next = createBlock(factory);
|
|
259
269
|
if (next === current) return;
|
|
260
270
|
if (current) removeChild(parent, current);
|