@vmz/core 0.3.9 → 0.3.11

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.
@@ -99,6 +99,14 @@ export type ComponentCtorLike = (new (props?: object) => DirectInstance) & {
99
99
  __vmzProps?: string[];
100
100
  __vmzWBInstalled?: boolean;
101
101
  __vmzCtorAppliesProps?: boolean;
102
+ /**
103
+ * Compile-time embedded BindingId → deps[] map. When the Rust generator
104
+ * emits this static map on the component class, `registerBind` resolves
105
+ * deps by lookup instead of rebuilding O(N×M) indices from the string[]
106
+ * the artifact passes per call. Absent (today) — falls back to the call
107
+ * site argument unchanged.
108
+ */
109
+ __vmzDepsMap?: Record<string | number, string[]>;
102
110
  name?: string;
103
111
  };
104
112
  export type DirectEachCtx = {
@@ -134,7 +142,9 @@ export type DirectApi = {
134
142
  adoptEnter: (node: Element) => boolean;
135
143
  adoptLeave: () => void;
136
144
  projectDefaultSlot: (hostEl: HTMLElement, node: Node) => void;
145
+ projectNamedSlot: (hostEl: HTMLElement, name: string, node: Node) => void;
137
146
  setHtml: (el: Element, value: unknown) => void;
147
+ setShow: (el: HTMLElement, condition: unknown) => void;
138
148
  bindHtml: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, el: Element) => void;
139
149
  bindComponentProp: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, hostInst: DirectInstance, propName: string) => void;
140
150
  component: (Ctor: ComponentCtorLike | string, props?: object) => HTMLElement;
@@ -3,6 +3,14 @@ export declare function settlePendingChildMounts(inst: any): Promise<void>;
3
3
  export declare function runDirectCreate(Component: any, inst: any): any;
4
4
  export declare function findOwnedDefaultSlot(root: any): any;
5
5
  export declare function findOwnedDefaultSlotTarget(root: any): any;
6
+ /**
7
+ * Owned `<slot name="…">` outlet for a named projection.
8
+ *
9
+ * Same ownership rule as {@link findOwnedDefaultSlot}: a nested component host
10
+ * (`[data-vmz]` / `__vmzInst`) is a boundary, so projecting `header` into a Card
11
+ * can never capture a Button's own named slot.
12
+ */
13
+ export declare function findOwnedNamedSlot(root: any, name: any): any;
6
14
  export declare function createInstance(Component: any, props?: any): any;
7
15
  export declare function destroy(inst: any): void;
8
16
  /**
@@ -13,15 +21,19 @@ export declare function destroy(inst: any): void;
13
21
  export declare function slotAnchorFor(node: any): any;
14
22
  export declare function insertProjectedSlotNode(target: any, node: any): any;
15
23
  /**
16
- * Hold projected children until the component's default slot exists.
24
+ * Hold projected children until the component slot exists.
17
25
  *
18
26
  * A slot inside an unmounted `v-if` branch (closed drawer/dialog, loading
19
27
  * skeleton) is absent at projection time. Appending to the component root would
20
28
  * paint the child into the live page, so keep it detached and splice it in when
21
29
  * the slot is mounted.
30
+ *
31
+ * `name` parks for a specific `slot name="…"` outlet. Named projections never
32
+ * fall back into the default slot: a missing named outlet keeps the node parked
33
+ * (or drops it on hosts without MutationObserver) instead of polluting it.
22
34
  */
23
- export declare function parkSlotNode(hostEl: any, node: any): void;
24
- /** Move parked children into the now-mounted default slot. Returns true when drained. */
35
+ export declare function parkSlotNode(hostEl: any, node: any, name?: any): void;
36
+ /** Move parked children into the now-mounted slot. Returns true when any drained. */
25
37
  export declare function flushParkedSlotNodes(hostEl: any): boolean;
26
38
  export declare function disposeDomTree(root: any): void;
27
39
  export declare function snapshotInstanceState(inst: any): {};
@@ -120,6 +120,37 @@ export function findOwnedDefaultSlotTarget(root) {
120
120
  };
121
121
  return walk(root);
122
122
  }
123
+ /**
124
+ * Owned `<slot name="…">` outlet for a named projection.
125
+ *
126
+ * Same ownership rule as {@link findOwnedDefaultSlot}: a nested component host
127
+ * (`[data-vmz]` / `__vmzInst`) is a boundary, so projecting `header` into a Card
128
+ * can never capture a Button's own named slot.
129
+ */
130
+ export function findOwnedNamedSlot(root, name) {
131
+ if (!root || root.nodeType !== 1)
132
+ return null;
133
+ if (name == null || name === '')
134
+ return null;
135
+ const wanted = String(name);
136
+ const tag = String(root.tagName || '').toLowerCase();
137
+ if (tag === 'slot' && root.getAttribute('name') === wanted)
138
+ return root;
139
+ const kids = root.children;
140
+ if (!kids || !kids.length)
141
+ return null;
142
+ for (let i = 0; i < kids.length; i++) {
143
+ const c = kids[i];
144
+ if (c.nodeType !== 1)
145
+ continue;
146
+ if (c.hasAttribute('data-vmz') || c.__vmzInst)
147
+ continue;
148
+ const hit = findOwnedNamedSlot(c, wanted);
149
+ if (hit)
150
+ return hit;
151
+ }
152
+ return null;
153
+ }
123
154
  export function createInstance(Component, props = {}) {
124
155
  if (precision.enabled)
125
156
  precision.componentExecs++;
@@ -203,17 +234,24 @@ export function insertProjectedSlotNode(target, node) {
203
234
  return anchor;
204
235
  }
205
236
  /**
206
- * Hold projected children until the component's default slot exists.
237
+ * Hold projected children until the component slot exists.
207
238
  *
208
239
  * A slot inside an unmounted `v-if` branch (closed drawer/dialog, loading
209
240
  * skeleton) is absent at projection time. Appending to the component root would
210
241
  * paint the child into the live page, so keep it detached and splice it in when
211
242
  * the slot is mounted.
243
+ *
244
+ * `name` parks for a specific `slot name="…"` outlet. Named projections never
245
+ * fall back into the default slot: a missing named outlet keeps the node parked
246
+ * (or drops it on hosts without MutationObserver) instead of polluting it.
212
247
  */
213
- export function parkSlotNode(hostEl, node) {
248
+ export function parkSlotNode(hostEl, node, name = null) {
214
249
  if (!hostEl || !node)
215
250
  return;
251
+ const slotName = name == null ? null : String(name);
216
252
  if (typeof MutationObserver === 'undefined') {
253
+ if (slotName != null)
254
+ return;
217
255
  // Non-browser DOM (unit hosts): preserve the legacy root fallback.
218
256
  const child = hostEl.__vmzInst;
219
257
  const root = (child && child.__vmzDomRoot) || hostEl;
@@ -223,8 +261,28 @@ export function parkSlotNode(hostEl, node) {
223
261
  hostEl.appendChild(node);
224
262
  return;
225
263
  }
226
- const parked = hostEl.__vmzSlotParked || (hostEl.__vmzSlotParked = []);
227
- parked.push(node);
264
+ if (slotName == null) {
265
+ const parked = hostEl.__vmzSlotParked || (hostEl.__vmzSlotParked = []);
266
+ parked.push(node);
267
+ }
268
+ else {
269
+ namedSlotParked(hostEl, slotName).push(node);
270
+ }
271
+ ensureSlotObserver(hostEl);
272
+ }
273
+ function namedSlotParked(hostEl, name) {
274
+ const map = hostEl.__vmzNamedSlotParked || (hostEl.__vmzNamedSlotParked = new Map());
275
+ const list = map.get(name);
276
+ if (list)
277
+ return list;
278
+ const fresh = [];
279
+ map.set(name, fresh);
280
+ return fresh;
281
+ }
282
+ function namedSlotAnchor(hostEl) {
283
+ return hostEl.__vmzNamedSlotAnchor || (hostEl.__vmzNamedSlotAnchor = new Map());
284
+ }
285
+ function ensureSlotObserver(hostEl) {
228
286
  if (hostEl.__vmzSlotObserver)
229
287
  return;
230
288
  hostEl.__vmzSlotObserver = new MutationObserver(() => {
@@ -235,44 +293,64 @@ export function parkSlotNode(hostEl, node) {
235
293
  hostEl.__vmzSlotObserver.observe(scope, { childList: true, subtree: true });
236
294
  }
237
295
  }
238
- /** Move parked children into the now-mounted default slot. Returns true when drained. */
296
+ /** Move parked children into the now-mounted slot. Returns true when any drained. */
239
297
  export function flushParkedSlotNodes(hostEl) {
240
298
  if (!hostEl)
241
299
  return false;
242
- const parked = hostEl.__vmzSlotParked;
243
- if (!parked || !parked.length)
244
- return false;
245
300
  const child = hostEl.__vmzInst;
246
301
  const root = (child && child.__vmzDomRoot) || hostEl;
247
302
  if (!root || root.nodeType !== 1)
248
303
  return false;
249
- const target = findOwnedDefaultSlotTarget(root);
250
- if (!target || !target.parentNode)
251
- return false;
252
- if (hostEl.__vmzSlotObserver) {
304
+ let drained = false;
305
+ const parkedDefault = hostEl.__vmzSlotParked;
306
+ if (parkedDefault && parkedDefault.length) {
307
+ const target = findOwnedDefaultSlotTarget(root);
308
+ if (target && target.parentNode) {
309
+ hostEl.__vmzSlotParked = [];
310
+ hostEl.__vmzSlotAnchor = insertParked(hostEl, target, parkedDefault);
311
+ drained = true;
312
+ }
313
+ }
314
+ const named = hostEl.__vmzNamedSlotParked;
315
+ if (named && named.size) {
316
+ for (const [name, nodes] of named) {
317
+ if (!nodes.length) {
318
+ named.delete(name);
319
+ continue;
320
+ }
321
+ const target = findOwnedNamedSlot(root, name);
322
+ if (!target || !target.parentNode)
323
+ continue;
324
+ named.delete(name);
325
+ namedSlotAnchor(hostEl).set(name, insertParked(hostEl, target, nodes));
326
+ drained = true;
327
+ }
328
+ }
329
+ const stillParked = (hostEl.__vmzSlotParked && hostEl.__vmzSlotParked.length > 0) ||
330
+ (hostEl.__vmzNamedSlotParked && hostEl.__vmzNamedSlotParked.size > 0);
331
+ if (drained && !stillParked && hostEl.__vmzSlotObserver) {
253
332
  hostEl.__vmzSlotObserver.disconnect();
254
333
  hostEl.__vmzSlotObserver = null;
255
334
  }
256
- hostEl.__vmzSlotParked = [];
335
+ return drained;
336
+ }
337
+ function insertParked(hostEl, target, parked) {
257
338
  const anchor = slotAnchorFor(parked[parked.length - 1]);
258
339
  const tag = String(target.tagName || '').toLowerCase();
259
340
  if (tag === 'slot') {
260
341
  const frag = typeof document !== 'undefined' ? document.createDocumentFragment() : null;
261
342
  if (!frag) {
262
343
  target.replaceWith(parked[0]);
263
- hostEl.__vmzSlotAnchor = anchor;
264
- return true;
344
+ return anchor;
265
345
  }
266
346
  for (const node of parked)
267
347
  frag.appendChild(node);
268
348
  target.replaceWith(frag);
269
- hostEl.__vmzSlotAnchor = anchor;
270
- return true;
349
+ return anchor;
271
350
  }
272
351
  for (const node of parked)
273
352
  target.appendChild(node);
274
- hostEl.__vmzSlotAnchor = anchor;
275
- return true;
353
+ return anchor;
276
354
  }
277
355
  export function disposeDomTree(root) {
278
356
  if (!root)
@@ -304,6 +382,10 @@ export function disposeDomTree(root) {
304
382
  node.__vmzSlotParked = [];
305
383
  if (node.__vmzSlotAnchor)
306
384
  node.__vmzSlotAnchor = null;
385
+ if (node.__vmzNamedSlotParked)
386
+ node.__vmzNamedSlotParked = null;
387
+ if (node.__vmzNamedSlotAnchor)
388
+ node.__vmzNamedSlotAnchor = null;
307
389
  let child = node.firstChild;
308
390
  while (child) {
309
391
  const next = child.nextSibling;
@@ -30,7 +30,7 @@ export declare const directApi: {
30
30
  attr(el: any, name: any, value: any): void;
31
31
  mergeClass(...parts: any[]): string;
32
32
  mergeStyle(...parts: any[]): string;
33
- on(el: any, type: any, handler: any): void;
33
+ on(el: any, type: any, handler: any, options?: any): void;
34
34
  /**
35
35
  * Bind a named instance method (no arrow / Function#toString).
36
36
  * Learns `skipFlush` after a sync invocation that schedules no dirty work
@@ -46,7 +46,25 @@ export declare const directApi: {
46
46
  */
47
47
  specFieldText(inst: any, bindingId: any, fieldName: any, textNode: any): void;
48
48
  setHtml(el: any, value: any): void;
49
+ /**
50
+ * Vue `v-text` — sets `textContent`, escaping all HTML (unlike `setHtml`).
51
+ * Overrides any existing children on the element. Static create-time path;
52
+ * reactive bindings use {@link bindText}.
53
+ */
54
+ setText(el: any, value: any): void;
55
+ /**
56
+ * Vue `v-show` — toggles `style.display` between empty string (visible,
57
+ * original CSS wins) and `'none'`. Element stays in the DOM tree (unlike
58
+ * `v-if`). Used for static-show values; reactive bindings emit inline
59
+ * patches in {@link trackPatch}.
60
+ */
61
+ setShow(el: any, condition: any): void;
49
62
  bindHtml(inst: any, bindingId: any, deps: any, get: any, el: any): void;
63
+ /**
64
+ * Reactive `v-text` textContent binding — mirrors {@link bindHtml} but
65
+ * writes `textContent` (HTML-escaped, safe).
66
+ */
67
+ bindText(inst: any, bindingId: any, deps: any, get: any, el: any): void;
50
68
  /**
51
69
  * Nested component (sync Direct child or island schedule).
52
70
  */
@@ -65,4 +83,13 @@ export declare const directApi: {
65
83
  * Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
66
84
  */
67
85
  projectDefaultSlot(hostEl: any, node: any): void;
86
+ /**
87
+ * Project parent children into a nested Direct component's `<slot name="…">`.
88
+ *
89
+ * Named projections are name-only (scope-slot values are not passed yet). The
90
+ * outlet search stops at nested component hosts, so a `header` projection never
91
+ * steals a nested Button's own named slot. A name with no outlet parks under that
92
+ * name — it must not fall through into the default slot.
93
+ */
94
+ projectNamedSlot(hostEl: any, name: any, node: any): void;
68
95
  };
@@ -2,7 +2,7 @@ import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normaliz
2
2
  import { applyDirectHostBox } from '../direct-host-box.js';
3
3
  import { createUnknownComponentElement } from '../unknown-component.js';
4
4
  import { applyDomAttr, eventPropHandlerName } from './attributes.js';
5
- import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
5
+ import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
6
6
  import { isEventEntryStrategy, noteDomCreate, noteDomRemove, scheduleClientOn } from './lifecycle.js';
7
7
  import { components, resolveComponent } from './registry.js';
8
8
  import { beginEventFlush, endEventFlush, inferHandlerMethod, methodAllowsSyncEventFlush, registerBind, runDomEventHandler, runPatch, scheduleRefresh, unregisterBind, } from '../reactivity/patch-scheduler.js';
@@ -169,7 +169,11 @@ export const directApi = {
169
169
  mergeStyle(...parts) {
170
170
  return mergeStyleParts(...parts);
171
171
  },
172
- on(el, type, handler) {
172
+ on(el, type, handler, options) {
173
+ // options: { capture?: boolean; passive?: boolean; once?: boolean }
174
+ const listenerOpts = options && (options.capture || options.passive || options.once)
175
+ ? { capture: !!options.capture, passive: !!options.passive, once: !!options.once }
176
+ : undefined;
173
177
  const inst = directApi._inst;
174
178
  if (directApi._eachCtx && typeof handler === 'function') {
175
179
  const bag = el.__vmzEvt || (el.__vmzEvt = Object.create(null));
@@ -191,7 +195,7 @@ export const directApi = {
191
195
  };
192
196
  bag[type] = listener;
193
197
  directApi._eachCtx.needDelegate(type);
194
- el.addEventListener(type, listener);
198
+ el.addEventListener(type, listener, listenerOpts);
195
199
  return;
196
200
  }
197
201
  // Infer once at bind time ??never Function#toString on the click hot path.
@@ -208,7 +212,7 @@ export const directApi = {
208
212
  return m.call(inst, ev);
209
213
  return handler.call(inst, ev);
210
214
  });
211
- });
215
+ }, listenerOpts);
212
216
  return;
213
217
  }
214
218
  el.addEventListener(type, (ev) => {
@@ -219,7 +223,7 @@ export const directApi = {
219
223
  if (typeof handler === 'function') {
220
224
  runDomEventHandler(inst, methodHint, () => handler.call(inst, ev));
221
225
  }
222
- });
226
+ }, listenerOpts);
223
227
  },
224
228
  /**
225
229
  * Bind a named instance method (no arrow / Function#toString).
@@ -227,6 +231,10 @@ export const directApi = {
227
231
  * (stride / transpose self-apply DOM).
228
232
  */
229
233
  onMethod(el, type, methodName, opts) {
234
+ // opts: { skipFlush?: boolean; capture?: boolean; passive?: boolean; once?: boolean }
235
+ const listenerOpts = opts && (opts.capture || opts.passive || opts.once)
236
+ ? { capture: !!opts.capture, passive: !!opts.passive, once: !!opts.once }
237
+ : undefined;
230
238
  // Prefer each-block owner: item create often runs during flush when `_inst` is unset.
231
239
  const eachOwner = directApi._eachCtx && directApi._eachCtx.inst;
232
240
  const inst = eachOwner || directApi._inst;
@@ -262,10 +270,10 @@ export const directApi = {
262
270
  const listener = (ev) => invoke(ev);
263
271
  bag[type] = listener;
264
272
  directApi._eachCtx.needDelegate(type);
265
- el.addEventListener(type, listener);
273
+ el.addEventListener(type, listener, listenerOpts);
266
274
  return;
267
275
  }
268
- el.addEventListener(type, (ev) => invoke(ev));
276
+ el.addEventListener(type, (ev) => invoke(ev), listenerOpts);
269
277
  },
270
278
  /**
271
279
  * Specialized single-field attr bind (0.1.29): compile-time field name, no generic get closure in artifact.
@@ -303,6 +311,23 @@ export const directApi = {
303
311
  setHtml(el, value) {
304
312
  el.innerHTML = value == null ? '' : String(value);
305
313
  },
314
+ /**
315
+ * Vue `v-text` — sets `textContent`, escaping all HTML (unlike `setHtml`).
316
+ * Overrides any existing children on the element. Static create-time path;
317
+ * reactive bindings use {@link bindText}.
318
+ */
319
+ setText(el, value) {
320
+ el.textContent = value == null ? '' : String(value);
321
+ },
322
+ /**
323
+ * Vue `v-show` — toggles `style.display` between empty string (visible,
324
+ * original CSS wins) and `'none'`. Element stays in the DOM tree (unlike
325
+ * `v-if`). Used for static-show values; reactive bindings emit inline
326
+ * patches in {@link trackPatch}.
327
+ */
328
+ setShow(el, condition) {
329
+ el.style.display = condition ? '' : 'none';
330
+ },
306
331
  bindHtml(inst, bindingId, deps, get, el) {
307
332
  directApi.trackPatch(inst, deps || [], function bindHtmlPatch() {
308
333
  let raw;
@@ -315,6 +340,22 @@ export const directApi = {
315
340
  el.innerHTML = raw == null ? '' : String(raw);
316
341
  }, bindingId);
317
342
  },
343
+ /**
344
+ * Reactive `v-text` textContent binding — mirrors {@link bindHtml} but
345
+ * writes `textContent` (HTML-escaped, safe).
346
+ */
347
+ bindText(inst, bindingId, deps, get, el) {
348
+ directApi.trackPatch(inst, deps || [], function bindTextPatch() {
349
+ let raw;
350
+ try {
351
+ raw = get.call(inst);
352
+ }
353
+ catch {
354
+ raw = '';
355
+ }
356
+ el.textContent = raw == null ? '' : String(raw);
357
+ }, bindingId);
358
+ },
318
359
  /**
319
360
  * Nested component (sync Direct child or island schedule).
320
361
  */
@@ -483,4 +524,41 @@ export const directApi = {
483
524
  // the component root, where it would otherwise leak into the rendered page.
484
525
  parkSlotNode(hostEl, node);
485
526
  },
527
+ /**
528
+ * Project parent children into a nested Direct component's `<slot name="…">`.
529
+ *
530
+ * Named projections are name-only (scope-slot values are not passed yet). The
531
+ * outlet search stops at nested component hosts, so a `header` projection never
532
+ * steals a nested Button's own named slot. A name with no outlet parks under that
533
+ * name — it must not fall through into the default slot.
534
+ */
535
+ projectNamedSlot(hostEl, name, node) {
536
+ if (!hostEl || node == null)
537
+ return;
538
+ const key = name == null ? '' : String(name);
539
+ if (!key || key === 'default') {
540
+ directApi.projectDefaultSlot(hostEl, node);
541
+ return;
542
+ }
543
+ const child = hostEl.__vmzInst;
544
+ let root = (child && child.__vmzDomRoot) || hostEl;
545
+ if (!root || root.nodeType !== 1)
546
+ root = hostEl;
547
+ const target = root && root.nodeType === 1 ? findOwnedNamedSlot(root, key) : null;
548
+ if (target && target.parentNode) {
549
+ const anchors = hostEl.__vmzNamedSlotAnchor || (hostEl.__vmzNamedSlotAnchor = new Map());
550
+ anchors.set(key, insertProjectedSlotNode(target, node));
551
+ return;
552
+ }
553
+ // The named `<slot>` was replaced by an earlier projected child: append after
554
+ // the last one for the same name instead of re-searching.
555
+ const anchors = hostEl.__vmzNamedSlotAnchor;
556
+ const anchor = anchors && anchors.get(key);
557
+ if (anchor && anchor.parentNode) {
558
+ anchors.set(key, slotAnchorFor(node));
559
+ anchor.parentNode.insertBefore(node, anchor.nextSibling);
560
+ return;
561
+ }
562
+ parkSlotNode(hostEl, node, key);
563
+ },
486
564
  };
@@ -16,6 +16,7 @@ export { settlePendingChildMounts } from './dom/component.js';
16
16
  export { runDirectCreate } from './dom/component.js';
17
17
  export { findOwnedDefaultSlot } from './dom/component.js';
18
18
  export { findOwnedDefaultSlotTarget } from './dom/component.js';
19
+ export { findOwnedNamedSlot } from './dom/component.js';
19
20
  export { createInstance } from './dom/component.js';
20
21
  export { destroy } from './dom/component.js';
21
22
  export { disposeDomTree } from './dom/component.js';
@@ -16,6 +16,7 @@ export { settlePendingChildMounts } from './dom/component.js';
16
16
  export { runDirectCreate } from './dom/component.js';
17
17
  export { findOwnedDefaultSlot } from './dom/component.js';
18
18
  export { findOwnedDefaultSlotTarget } from './dom/component.js';
19
+ export { findOwnedNamedSlot } from './dom/component.js';
19
20
  export { createInstance } from './dom/component.js';
20
21
  export { destroy } from './dom/component.js';
21
22
  export { disposeDomTree } from './dom/component.js';
@@ -645,6 +645,9 @@ function reindexBindingDeps(inst, bindingId, deps) {
645
645
  const entry = inst.__vmzBindings[bindingId];
646
646
  if (!entry)
647
647
  return;
648
+ // Fast path: deps 完全相同 → 无需重建 dep→bindings 索引。
649
+ if (arraysEqual(entry.deps, deps))
650
+ return;
648
651
  for (const dep of entry.deps || []) {
649
652
  const list = inst.__vmzDepToBindings[dep];
650
653
  if (!list)
@@ -664,10 +667,47 @@ function reindexBindingDeps(inst, bindingId, deps) {
664
667
  }
665
668
  }
666
669
  }
670
+ function arraysEqual(a, b) {
671
+ if (a === b)
672
+ return true;
673
+ const la = a ? a.length : 0;
674
+ const lb = b ? b.length : 0;
675
+ if (la !== lb)
676
+ return false;
677
+ for (let i = 0; i < la; i++)
678
+ if (a[i] !== b[i])
679
+ return false;
680
+ return true;
681
+ }
682
+ /**
683
+ * Resolve deps for a binding: prefer the argument from generated artifact,
684
+ * fall back to a compile-time-embedded static map on the component class.
685
+ *
686
+ * The Rust generator will later emit `Ctor.__vmzDepsMap = { [bindingId]: deps[] }`
687
+ * so runtime can do a single lookup instead of rebuilding O(N×M) dep→binding
688
+ * indices from the string[] the artifact passes today.
689
+ */
690
+ function resolveDeps(inst, depsArg, bindingId) {
691
+ if (depsArg && depsArg.length)
692
+ return depsArg;
693
+ if (bindingId == null)
694
+ return depsArg || [];
695
+ const ctor = inst && inst.constructor;
696
+ const map = ctor && ctor.__vmzDepsMap;
697
+ if (map) {
698
+ const hit = map[bindingId];
699
+ if (hit)
700
+ return hit;
701
+ }
702
+ return depsArg || [];
703
+ }
667
704
  export function registerBind(inst, deps, fn, bindingId = null) {
705
+ // 查表优先: generator 侧若已在 Ctor.__vmzDepsMap 嵌入完整 deps map
706
+ // 则直接取用, 避免每次 registerBind 都用传入 string[] 重建索引.
707
+ const resolvedDeps = resolveDeps(inst, deps, bindingId);
668
708
  if (!inst.__vmzBinders)
669
709
  inst.__vmzBinders = Object.create(null);
670
- for (const dep of deps || []) {
710
+ for (const dep of resolvedDeps) {
671
711
  if (!inst.__vmzBinders[dep])
672
712
  inst.__vmzBinders[dep] = [];
673
713
  // Dedup: cf untrack/retrack with identical deps must not grow the binder list.
@@ -685,7 +725,7 @@ export function registerBind(inst, deps, fn, bindingId = null) {
685
725
  }
686
726
  if (!entry.patches.includes(fn))
687
727
  entry.patches.push(fn);
688
- reindexBindingDeps(inst, bindingId, deps || []);
728
+ reindexBindingDeps(inst, bindingId, resolvedDeps);
689
729
  }
690
730
  export function unregisterBind(inst, deps, fn, bindingId = null) {
691
731
  const binders = inst.__vmzBinders;
@@ -307,6 +307,64 @@ function makeVirtualEl(tag) {
307
307
  });
308
308
  return node;
309
309
  }
310
+ /**
311
+ * Owned `<slot>` outlet in a serialize el tree.
312
+ *
313
+ * `name === null` matches the default outlet (no `name` attr). Named lookups require
314
+ * an exact match and stop at nested component shells (`data-vmz`), mirroring the
315
+ * browser runtime ownership boundary.
316
+ */
317
+ function findOwnedSerializeSlot(n, name) {
318
+ if (!n || typeof n !== 'object')
319
+ return null;
320
+ if (n.__kind === 'frag') {
321
+ for (const c of n.children || []) {
322
+ const hit = findOwnedSerializeSlot(c, name);
323
+ if (hit)
324
+ return hit;
325
+ }
326
+ return null;
327
+ }
328
+ if (n.__kind !== 'el')
329
+ return null;
330
+ if (n.tag === 'slot') {
331
+ const outletName = n.attrs && n.attrs.name != null ? String(n.attrs.name) : '';
332
+ if (name == null ? !outletName : outletName === name)
333
+ return n;
334
+ }
335
+ for (const c of n.children || []) {
336
+ if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
337
+ continue;
338
+ const hit = findOwnedSerializeSlot(c, name);
339
+ if (hit)
340
+ return hit;
341
+ }
342
+ return null;
343
+ }
344
+ function findOwnedSerializeSlotTarget(n, name) {
345
+ const slot = findOwnedSerializeSlot(n, name);
346
+ if (slot)
347
+ return slot;
348
+ if (name != null)
349
+ return null;
350
+ if (!n || n.__kind !== 'el')
351
+ return null;
352
+ const tag = String(n.tag || '').toLowerCase();
353
+ if ((tag === 'button' || tag === 'a') && n.attrs && n.attrs['data-vmz-ui'] === 'button')
354
+ return n;
355
+ if (n.attrs && n.attrs['data-vmz'] != null)
356
+ return null;
357
+ for (const c of n.children || []) {
358
+ if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
359
+ continue;
360
+ const hit = findOwnedSerializeSlotTarget(c, name);
361
+ if (hit)
362
+ return hit;
363
+ }
364
+ return null;
365
+ }
366
+ /** Outlets that already received projected content (fallback children dropped once). */
367
+ const filledSerializeSlots = new WeakSet();
310
368
  const serializeApi = {
311
369
  _inst: null,
312
370
  _branchBinds: null,
@@ -476,69 +534,25 @@ const serializeApi = {
476
534
  return;
477
535
  // serializeApi.component returns a serialize el tree (or island shell).
478
536
  const root = hostEl.__kind === 'el' ? hostEl : null;
479
- const findOwnedSlot = (n) => {
480
- if (!n || typeof n !== 'object')
481
- return null;
482
- if (n.__kind === 'frag') {
483
- for (const c of n.children || []) {
484
- const hit = findOwnedSlot(c);
485
- if (hit)
486
- return hit;
487
- }
488
- return null;
489
- }
490
- if (n.__kind !== 'el')
491
- return null;
492
- if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
493
- return n;
494
- for (const c of n.children || []) {
495
- if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
496
- continue;
497
- const hit = findOwnedSlot(c);
498
- if (hit)
499
- return hit;
500
- }
501
- return null;
502
- };
503
- const findOwnedSlotTarget = (n) => {
504
- const slot = findOwnedSlot(n);
505
- if (slot)
506
- return slot;
507
- if (!n || n.__kind !== 'el')
508
- return null;
509
- const tag = String(n.tag || '').toLowerCase();
510
- if ((tag === 'button' || tag === 'a') && n.attrs && n.attrs['data-vmz-ui'] === 'button')
511
- return n;
512
- if (n.attrs && n.attrs['data-vmz'] != null)
513
- return null;
514
- for (const c of n.children || []) {
515
- if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
516
- continue;
517
- const hit = findOwnedSlotTarget(c);
518
- if (hit)
519
- return hit;
520
- }
521
- return null;
522
- };
523
537
  // Prefer searching the component body (first child of host wrapper).
524
538
  let target = null;
525
539
  if (root) {
526
540
  for (const c of root.children || []) {
527
541
  if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
528
542
  continue;
529
- target = findOwnedSlotTarget(c);
543
+ target = findOwnedSerializeSlotTarget(c, null);
530
544
  if (target)
531
545
  break;
532
546
  }
533
547
  if (!target)
534
- target = findOwnedSlotTarget(root);
548
+ target = findOwnedSerializeSlotTarget(root, null);
535
549
  }
536
550
  if (target) {
537
551
  if (target.tag === 'slot') {
538
552
  target.__rawHtml = null;
539
553
  if (!Array.isArray(target.children))
540
554
  target.children = [];
541
- // Append ??multiple projectDefaultSlot calls must accumulate (SSR).
555
+ // Append — multiple projectDefaultSlot calls must accumulate (SSR).
542
556
  // Client path replaces the live <slot> then appends siblings; serialize must push.
543
557
  target.children.push(node);
544
558
  return;
@@ -552,6 +566,46 @@ const serializeApi = {
552
566
  // mounted (closed overlay, loading skeleton). Mirror the browser runtime and
553
567
  // drop them instead of painting them into the component root.
554
568
  },
569
+ /**
570
+ * Project parent children into a nested component's `<slot name="…">` outlet.
571
+ *
572
+ * Named projections are name-only (scope-slot values are not passed yet). Only an
573
+ * outlet with the exact `name` accepts the node — a missing name drops it instead
574
+ * of landing in the default slot. Fallback children stop rendering once the outlet
575
+ * has projected content.
576
+ */
577
+ projectNamedSlot(hostEl, name, node) {
578
+ if (!hostEl || node == null)
579
+ return;
580
+ const key = name == null ? '' : String(name);
581
+ if (!key || key === 'default') {
582
+ serializeApi.projectDefaultSlot(hostEl, node);
583
+ return;
584
+ }
585
+ const root = hostEl.__kind === 'el' ? hostEl : null;
586
+ let target = null;
587
+ if (root) {
588
+ for (const c of root.children || []) {
589
+ if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
590
+ continue;
591
+ target = findOwnedSerializeSlotTarget(c, key);
592
+ if (target)
593
+ break;
594
+ }
595
+ if (!target)
596
+ target = findOwnedSerializeSlotTarget(root, key);
597
+ }
598
+ if (!target)
599
+ return;
600
+ target.__rawHtml = null;
601
+ if (!Array.isArray(target.children))
602
+ target.children = [];
603
+ if (!filledSerializeSlots.has(target)) {
604
+ filledSerializeSlots.add(target);
605
+ target.children.length = 0;
606
+ }
607
+ target.children.push(node);
608
+ },
555
609
  setHtml(el, value) {
556
610
  if (!el || el.__kind !== 'el')
557
611
  return;
package/package.json CHANGED
@@ -68,12 +68,12 @@
68
68
  "main": "./dist/faces/server.js",
69
69
  "name": "@vmz/core",
70
70
  "optionalDependencies": {
71
- "@vmz/vmz-darwin-arm64": "0.3.9",
72
- "@vmz/vmz-darwin-x64": "0.3.9",
73
- "@vmz/vmz-linux-arm64": "0.3.9",
74
- "@vmz/vmz-linux-x64": "0.3.9",
75
- "@vmz/vmz-win32-arm64": "0.3.9",
76
- "@vmz/vmz-win32-x64": "0.3.9"
71
+ "@vmz/vmz-darwin-arm64": "0.3.11",
72
+ "@vmz/vmz-darwin-x64": "0.3.11",
73
+ "@vmz/vmz-linux-arm64": "0.3.11",
74
+ "@vmz/vmz-linux-x64": "0.3.11",
75
+ "@vmz/vmz-win32-arm64": "0.3.11",
76
+ "@vmz/vmz-win32-x64": "0.3.11"
77
77
  },
78
78
  "publishConfig": {
79
79
  "access": "public"
@@ -88,5 +88,5 @@
88
88
  },
89
89
  "type": "module",
90
90
  "types": "./dist/faces/index.d.ts",
91
- "version": "0.3.9"
91
+ "version": "0.3.11"
92
92
  }