@vobs/runtime 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/async-boundary.cjs.map +1 -1
  2. package/dist/async-boundary.js +1 -1
  3. package/dist/async-boundary.js.map +1 -1
  4. package/dist/bind.cjs +18 -0
  5. package/dist/bind.cjs.map +1 -1
  6. package/dist/bind.js +19 -1
  7. package/dist/bind.js.map +1 -1
  8. package/dist/boundary.cjs.map +1 -1
  9. package/dist/boundary.js +1 -1
  10. package/dist/boundary.js.map +1 -1
  11. package/dist/dynamic.cjs.map +1 -1
  12. package/dist/dynamic.js +1 -1
  13. package/dist/dynamic.js.map +1 -1
  14. package/dist/error-boundary.cjs +5 -1
  15. package/dist/error-boundary.cjs.map +1 -1
  16. package/dist/error-boundary.d.cts +7 -3
  17. package/dist/error-boundary.d.ts +7 -3
  18. package/dist/error-boundary.js +6 -2
  19. package/dist/error-boundary.js.map +1 -1
  20. package/dist/fragment.cjs.map +1 -1
  21. package/dist/fragment.js +1 -1
  22. package/dist/fragment.js.map +1 -1
  23. package/dist/hmr.cjs +11 -0
  24. package/dist/hmr.cjs.map +1 -1
  25. package/dist/hmr.d.cts +7 -1
  26. package/dist/hmr.d.ts +7 -1
  27. package/dist/hmr.js +10 -0
  28. package/dist/hmr.js.map +1 -1
  29. package/dist/index.cjs +37 -3
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +37 -4
  34. package/dist/index.js.map +1 -1
  35. package/dist/ops.cjs +21 -2
  36. package/dist/ops.cjs.map +1 -1
  37. package/dist/ops.js +22 -3
  38. package/dist/ops.js.map +1 -1
  39. package/dist/profiler.cjs.map +1 -1
  40. package/dist/profiler.js +1 -1
  41. package/dist/profiler.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/dynamic.test.ts +65 -1
  44. package/src/error-boundary.ts +13 -3
  45. package/src/hmr.test.ts +17 -0
  46. package/src/hmr.ts +14 -0
  47. package/src/index.ts +1 -0
  48. package/src/ops.ts +38 -3
  49. package/src/props.test.ts +32 -1
package/src/hmr.test.ts CHANGED
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
3
3
  import {
4
4
  createHmrStateStore,
5
5
  disposeHmrModule,
6
+ hmrStateRef,
6
7
  resolveComponent,
7
8
  updateHmrModule
8
9
  } from './hmr'
@@ -54,3 +55,19 @@ describe('runtime HMR', () => {
54
55
  expect(createHmrStateStore(moduleId).get('count', 0)).toBe(3)
55
56
  })
56
57
  })
58
+
59
+ describe('hmrStateRef', () => {
60
+ it('模块重执行时复用既有信号实例(状态保鲜)', () => {
61
+ let first: unknown
62
+ const create = () => ({ value: 0, tag: Math.random() })
63
+ // 模拟模块首执行
64
+ const firstRef = hmrStateRef('src/stores/a.ts', 'count', create)
65
+ first = firstRef
66
+ // 模拟热更新后模块重执行:新实例、同一 key
67
+ const secondRef = hmrStateRef('src/stores/a.ts', 'count', create)
68
+ expect(secondRef).toBe(first)
69
+ // 不同 key / 不同模块各自独立
70
+ expect(hmrStateRef('src/stores/a.ts', 'other', create)).not.toBe(first)
71
+ expect(hmrStateRef('src/stores/b.ts', 'count', create)).not.toBe(first)
72
+ })
73
+ })
package/src/hmr.ts CHANGED
@@ -96,6 +96,20 @@ export function createHmrStateStore(moduleId: string): HmrStateStore {
96
96
  }
97
97
  }
98
98
 
99
+ /**
100
+ * 编译器为模块顶层 state() 声明生成的取值入口(`#state:` 前缀与手动 store 键隔离)。
101
+ * 模块热更新重执行时复用既有信号实例:旧导入方持有的实例与新模块实例共享同一份状态,
102
+ * 消除"两份模块、两份状态"导致的编辑不生效/页面半边失灵。
103
+ */
104
+ export function hmrStateRef<T>(moduleId: string, key: string, create: () => T): T {
105
+ const state = getModule(moduleId).state
106
+ const registryKey = `#state:${key}`
107
+ if (state.has(registryKey)) return state.get(registryKey) as T
108
+ const value = create()
109
+ state.set(registryKey, value)
110
+ return value
111
+ }
112
+
99
113
  export function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {
100
114
  const instances = getModule(moduleId).instances
101
115
  instances.add(instance)
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export type {
65
65
  export {
66
66
  createHmrStateStore,
67
67
  disposeHmrModule,
68
+ hmrStateRef,
68
69
  markHmrInstanceMounted,
69
70
  registerHmrInstance,
70
71
  resolveComponent,
package/src/ops.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // 编译产物调用的基础操作
2
2
 
3
- import { createOwner, getCurrentOwner, setOwnerDebugName, type Owner } from '@vobs/reactivity'
3
+ import { createOwner, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'
4
4
  import { isVobsFragment, type VobsNode } from './fragment'
5
5
  import { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'
6
6
  import {
@@ -118,6 +118,9 @@ export function setProperty(
118
118
  ? readDebugValue(() => Reflect.get(node, key))
119
119
  : undefined
120
120
  getRenderer().setProperty(node, key, value)
121
+ if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {
122
+ scheduleSelectValueSync(node, value)
123
+ }
121
124
  if (getRuntimeDebugHooks()) {
122
125
  invokeRuntimeDebug('domMutation', {
123
126
  operation: 'property',
@@ -129,6 +132,30 @@ export function setProperty(
129
132
  }
130
133
  }
131
134
 
135
+ const pendingSelectValues = new WeakMap<Element, unknown>()
136
+ const selectSyncScheduled = new WeakSet<Element>()
137
+
138
+ /**
139
+ * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value
140
+ * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值
141
+ * (消费方此前只能用 ref + queueMicrotask 规避)。
142
+ * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内
143
+ * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,
144
+ * 由 value 的绑定 effect 在后续信号更新中正常覆盖。
145
+ */
146
+ function scheduleSelectValueSync(node: Element, value: unknown): void {
147
+ pendingSelectValues.set(node, value)
148
+ if (selectSyncScheduled.has(node)) return
149
+ selectSyncScheduled.add(node)
150
+ queueMicrotask(() => {
151
+ selectSyncScheduled.delete(node)
152
+ if (!pendingSelectValues.has(node)) return
153
+ const pending = pendingSelectValues.get(node)
154
+ pendingSelectValues.delete(node)
155
+ getRenderer().setProperty(node, 'value', pending)
156
+ })
157
+ }
158
+
132
159
  export function setAttribute(
133
160
  node: Element,
134
161
  key: string,
@@ -207,6 +234,9 @@ export function addEventListener(
207
234
  if (previous && previous.original === handler && previous.owner === owner) return
208
235
  if (previous) renderer.removeEventListener(node, event, previous.handler)
209
236
  const listener = owner ? (reason: Event) => {
237
+ // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。
238
+ // 否则 owner.run 会抛"已销毁的 Owner",在事件流里制造无意义的错误噪音。
239
+ if (owner.disposed) return
210
240
  try {
211
241
  owner.run(() => handler(reason))
212
242
  } catch (error) {
@@ -273,7 +303,12 @@ export function createComponent<Component extends VobsComponent>(
273
303
  })
274
304
  let node: VobsNode
275
305
  try {
276
- node = owner.run(() => component(props))
306
+ // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值
307
+ // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,
308
+ // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发
309
+ // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。
310
+ // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。
311
+ node = owner.run(() => untrack(() => component(props)))
277
312
  } catch (error) {
278
313
  owner.dispose()
279
314
  attachSourceLocation(error, source)
@@ -288,7 +323,7 @@ export function createComponent<Component extends VobsComponent>(
288
323
  parent: null,
289
324
  refresh(): void {
290
325
  const previous = instance.node
291
- const next = owner.run(() => component(props))
326
+ const next = owner.run(() => untrack(() => component(props)))
292
327
  if (instance.parent && !isVobsFragment(previous) && !isVobsFragment(next)) {
293
328
  getRenderer().insertBefore(instance.parent, next, previous)
294
329
  getRenderer().removeChild(instance.parent, previous)
package/src/props.test.ts CHANGED
@@ -1,10 +1,41 @@
1
1
  // @vitest-environment jsdom
2
2
  import { describe, expect, it } from 'vitest'
3
3
  import { createDOMRenderer, setRenderer } from '@vobs/vobs'
4
- import { setStaticProps, spreadProps } from './index'
4
+ import { setProperty, setStaticProps, spreadProps } from './index'
5
5
 
6
6
  setRenderer(createDOMRenderer())
7
7
 
8
+ describe('select value sync', () => {
9
+ it('option 子节点就绪后重放 value 赋值(微任务)', async () => {
10
+ const select = document.createElement('select') as HTMLSelectElement
11
+ // 模拟编译产物顺序:先设置 value,后插入 option 子节点
12
+ setProperty(select, 'value', 'b')
13
+ select.innerHTML = '<option value="a">A</option><option value="b">B</option>'
14
+ // 子节点插入使 value 回落到首个 option
15
+ expect(select.value).toBe('a')
16
+ await Promise.resolve()
17
+ expect(select.value).toBe('b')
18
+ })
19
+
20
+ it('setStaticProps 的 value 走同一重放路径', async () => {
21
+ const select = document.createElement('select') as HTMLSelectElement
22
+ setStaticProps(select, { value: 'x' })
23
+ select.innerHTML = '<option value="x">X</option>'
24
+ expect(select.value).toBe('x')
25
+ await Promise.resolve()
26
+ expect(select.value).toBe('x')
27
+ })
28
+
29
+ it('同一微任务内多次赋值以最后一次为准', async () => {
30
+ const select = document.createElement('select') as HTMLSelectElement
31
+ setProperty(select, 'value', 'a')
32
+ setProperty(select, 'value', 'c')
33
+ select.innerHTML = '<option value="a">A</option><option value="c">C</option>'
34
+ await Promise.resolve()
35
+ expect(select.value).toBe('c')
36
+ })
37
+ })
38
+
8
39
  describe('static and spread prop application', () => {
9
40
  it('spreadProps applies false for property keys to clear them', () => {
10
41
  const node = document.createElement('input') as HTMLInputElement