@vmz/core 0.3.13 → 0.3.15

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.
@@ -5,20 +5,17 @@ import type { ComponentCtorLike, DirectInstance } from './direct-api.types.js';
5
5
  export type HydrateFn = (Ctor: ComponentCtorLike, root: Element, props: object) => Promise<unknown>;
6
6
  export type HydrateRouteFn = (Page: ComponentCtorLike, root: Element, props: object, layouts?: ComponentCtorLike[]) => Promise<unknown>;
7
7
  export type HydrateRoutePageFn = (Page: ComponentCtorLike, root: Element, props: object) => Promise<unknown>;
8
- export type DomFallbackModule = {
9
- hydrate?: HydrateFn;
10
- hydrateRoute?: HydrateRouteFn;
11
- hydrateRoutePage?: HydrateRoutePageFn;
12
- };
13
8
  export type ClientNavOpts = {
14
9
  fetchImpl?: typeof fetch;
15
10
  document?: Document;
16
11
  history?: History;
17
12
  location?: Location;
13
+ /** Packed face 的 hydrate,由生成入口静态引入;页面导航缺失时显式报错,不回落到未打包路径。 */
18
14
  hydrate?: HydrateFn;
19
15
  hydrateRoute?: HydrateRouteFn;
20
16
  hydrateRoutePage?: HydrateRoutePageFn;
21
17
  destroy?: (inst: DirectInstance | object) => void;
18
+ /** Pack manifest 的 chunk 查表(生成入口的 import 表);缺失时页面导航显式报错。 */
22
19
  importPage?: (chunkId: string) => Promise<unknown>;
23
20
  };
24
21
  export type TransitionToOpts = {
@@ -127,6 +127,10 @@ export type DirectApi = {
127
127
  el: (tag: string) => Element;
128
128
  text: (value?: unknown) => Text;
129
129
  frag: () => DocumentFragment;
130
+ /** Multi-root `<template v-for>` row fragment (scoped on resume by key). */
131
+ fragRow: (key?: unknown) => DocumentFragment;
132
+ /** Closes the row scope opened by `fragRow`. */
133
+ rowEnd: () => void;
130
134
  comment: (value?: string) => Comment;
131
135
  attr: (el: Element, name: string, value: unknown) => void;
132
136
  mergeClass: (...parts: unknown[]) => string;
@@ -140,12 +144,19 @@ export type DirectApi = {
140
144
  onMethod: (el: Element, type: string, method: string) => void;
141
145
  onComponentEvent: (el: Element, type: string, method: string) => void;
142
146
  adoptEnter: (node: Element) => boolean;
147
+ adoptEnterSlot: (hostEl: Element, name: string) => boolean;
143
148
  adoptLeave: () => void;
144
149
  projectDefaultSlot: (hostEl: HTMLElement, node: Node) => void;
145
150
  projectNamedSlot: (hostEl: HTMLElement, name: string, node: Node) => void;
146
151
  setHtml: (el: Element, value: unknown) => void;
152
+ /** `v-html` write that keeps the SSR subtree when its payload digest matches. */
153
+ html: (el: Element, value: unknown) => void;
147
154
  setShow: (el: HTMLElement, condition: unknown) => void;
148
155
  bindHtml: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, el: Element) => void;
149
156
  bindComponentProp: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, hostInst: DirectInstance, propName: string) => void;
157
+ bindComponentProps: (inst: DirectInstance, hostEl: HTMLElement, slot: string, deps: string[], get: PatchFn) => void;
158
+ bindComponentEvents: (inst: DirectInstance, hostEl: HTMLElement, slot: string, deps: string[], get: PatchFn) => void;
159
+ bindObjectListeners: (el: Element, slot: string, source: unknown, inst: DirectInstance | null, mode: 'bind' | 'on') => void;
160
+ spreadProps: (source: unknown) => Record<string, unknown>;
150
161
  component: (Ctor: ComponentCtorLike | string, props?: object) => HTMLElement;
151
162
  };
@@ -9,3 +9,14 @@ export declare function resolveDirectHostBox(name: string, Ctor: ComponentCtor |
9
9
  export declare function applyDirectHostBox(host: HTMLElement, name: string, Ctor: ComponentCtor | null | undefined): void;
10
10
  /** SSR attrs fragment for host box (only when contents). */
11
11
  export declare function directHostBoxStyleAttr(name: string, Ctor: ComponentCtor | null | undefined): string | null;
12
+ /**
13
+ * Host element tag (`div` vs `span`).
14
+ *
15
+ * `display: contents` chips are phrasing content, so a `<div>` host is invalid
16
+ * HTML inside `<p>`. The browser parser then closes the `<p>` early and moves
17
+ * the host (and its trailing text) out of it, which desynchronizes the SSR tree
18
+ * from the client tree and makes resume rebuild those nodes. Emitting a `<span>`
19
+ * host keeps the SSR markup parse-stable: the parsed tree matches the tree the
20
+ * client `__vmzCreate` builds.
21
+ */
22
+ export declare function resolveDirectHostTag(name: string, Ctor: ComponentCtor | null | undefined): 'div' | 'span';
@@ -23,3 +23,16 @@ export function applyDirectHostBox(host, name, Ctor) {
23
23
  export function directHostBoxStyleAttr(name, Ctor) {
24
24
  return resolveDirectHostBox(name, Ctor) === 'contents' ? 'display:contents' : null;
25
25
  }
26
+ /**
27
+ * Host element tag (`div` vs `span`).
28
+ *
29
+ * `display: contents` chips are phrasing content, so a `<div>` host is invalid
30
+ * HTML inside `<p>`. The browser parser then closes the `<p>` early and moves
31
+ * the host (and its trailing text) out of it, which desynchronizes the SSR tree
32
+ * from the client tree and makes resume rebuild those nodes. Emitting a `<span>`
33
+ * host keeps the SSR markup parse-stable: the parsed tree matches the tree the
34
+ * client `__vmzCreate` builds.
35
+ */
36
+ export function resolveDirectHostTag(name, Ctor) {
37
+ return resolveDirectHostBox(name, Ctor) === 'contents' ? 'span' : 'div';
38
+ }
@@ -1,4 +1,10 @@
1
1
  import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normalize.js';
2
+ /**
3
+ * `xlink:href` parsed from SSR HTML lives in the XLink namespace. A plain
4
+ * `setAttribute` writes a non-namespaced attribute instead, so the SVG host
5
+ * (`<use>`) no longer resolves the reference after hydration or client create.
6
+ */
7
+ const XLINK_NS = 'http://www.w3.org/1999/xlink';
2
8
  export const BOOLEAN_HTML_ATTRS = new Set([
3
9
  'disabled',
4
10
  'checked',
@@ -21,6 +27,49 @@ export const BOOLEAN_HTML_ATTRS = new Set([
21
27
  'default',
22
28
  'inert',
23
29
  ]);
30
+ /**
31
+ * A `<select>` is patched before its `<option>` children are appended: emit
32
+ * writes the model attr while the select is still empty, so the first write
33
+ * selects nothing and the model never changes again. Remember the pending
34
+ * value and replay it once the synchronous create pass has finished.
35
+ */
36
+ const pendingSelectValues = new WeakMap();
37
+ /** First `value` write seeds a textarea's default value once. */
38
+ const seededTextareaDefaults = new WeakSet();
39
+ /**
40
+ * `form.reset()` restores the *default* state, which browsers derive from the
41
+ * `selected`, `value` and `checked` attributes. The first write therefore
42
+ * records the initial model there, and later model updates move only the IDL
43
+ * state (or `selected` on the options) instead of rewriting the defaults.
44
+ */
45
+ const seededSelectDefaults = new WeakSet();
46
+ const seededInputValueDefaults = new WeakSet();
47
+ const seededCheckedDefaults = new WeakSet();
48
+ const pendingSelectReplays = new WeakSet();
49
+ /** Checkbox and radio `value` attributes are submitted values, not defaults. */
50
+ function isCheckableInput(el) {
51
+ const type = String(el.type || (el.getAttribute && el.getAttribute('type')) || 'text').toLowerCase();
52
+ return type === 'checkbox' || type === 'radio';
53
+ }
54
+ function replaySelectValue(el) {
55
+ if (!pendingSelectValues.has(el))
56
+ return;
57
+ const value = pendingSelectValues.get(el);
58
+ const opts = el.options || el.querySelectorAll?.('option') || [];
59
+ if (!opts.length)
60
+ return;
61
+ pendingSelectValues.delete(el);
62
+ applyDomAttr(el, 'value', value);
63
+ }
64
+ function deferSelectValue(el) {
65
+ if (pendingSelectReplays.has(el))
66
+ return;
67
+ pendingSelectReplays.add(el);
68
+ queueMicrotask(() => {
69
+ pendingSelectReplays.delete(el);
70
+ replaySelectValue(el);
71
+ });
72
+ }
24
73
  export function applyDomAttr(el, name, value) {
25
74
  const key = name === 'className' ? 'class' : name;
26
75
  if (key === 'class') {
@@ -39,11 +88,36 @@ export function applyDomAttr(el, name, value) {
39
88
  el.removeAttribute('style');
40
89
  return;
41
90
  }
91
+ if (String(key).startsWith('xlink:') && el && typeof el.setAttributeNS === 'function') {
92
+ const local = String(key).slice('xlink:'.length);
93
+ if (value == null || value === false) {
94
+ if (typeof el.removeAttributeNS === 'function')
95
+ el.removeAttributeNS(XLINK_NS, local);
96
+ else
97
+ el.removeAttribute(key);
98
+ }
99
+ else {
100
+ el.setAttributeNS(XLINK_NS, `xlink:${local}`, value === true ? '' : String(value));
101
+ }
102
+ return;
103
+ }
42
104
  if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
43
105
  const on = value === true || value === '';
44
- if (key === 'checked' && el && el.tagName === 'INPUT' && String(el.type || '').toLowerCase() === 'checkbox') {
106
+ // Reflect `checked` through its IDL property. Once the user has interacted
107
+ // with a radio, attribute writes only reach its default state, so a later
108
+ // model update would otherwise leave the visible control stale. Only the
109
+ // first write records the default; later ones stay on the IDL.
110
+ if (key === 'checked' && el && el.tagName === 'INPUT') {
111
+ if (!seededCheckedDefaults.has(el)) {
112
+ seededCheckedDefaults.add(el);
113
+ if (on)
114
+ el.setAttribute('checked', '');
115
+ else
116
+ el.removeAttribute('checked');
117
+ }
45
118
  if (el.checked !== on)
46
119
  el.checked = on;
120
+ return;
47
121
  }
48
122
  if (value === false || value == null || value === '') {
49
123
  el.removeAttribute(key);
@@ -57,16 +131,82 @@ export function applyDomAttr(el, name, value) {
57
131
  // also need the IDL `.value` property so controlled updates stay in sync after switches.
58
132
  // linkedom `<select>.value` is getter-only ??sync via `option.selected` instead of throwing.
59
133
  if (key === 'value' && el && (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT' || el.tagName === 'SELECT')) {
134
+ // A `multiple` select model is a list: every option in the model is
135
+ // selected and there is no single `value` attribute to reflect.
136
+ const listed = Array.isArray(value) ? value.map((v) => String(v)) : null;
60
137
  const next = value == null || value === false ? '' : String(value);
61
138
  if (el.tagName === 'SELECT') {
62
139
  const opts = el.options || el.querySelectorAll?.('option') || [];
140
+ if (!opts.length) {
141
+ pendingSelectValues.set(el, value);
142
+ deferSelectValue(el);
143
+ return;
144
+ }
145
+ pendingSelectValues.delete(el);
146
+ // The `selected` attribute is the option's default state, the value
147
+ // `form.reset()` restores. Seed it with the initial model once and
148
+ // move only `option.selected` afterwards. Attribute writes run after
149
+ // the IDL pass: setting selectedness marks the option dirty, so the
150
+ // later attribute write records the default without moving the
151
+ // visible selection.
152
+ const seed = !seededSelectDefaults.has(el);
153
+ if (seed)
154
+ seededSelectDefaults.add(el);
155
+ const selections = [];
63
156
  for (const opt of opts) {
64
- opt.selected = String(opt.value ?? '') === next;
157
+ // Match SSR `markSelectSelection`: an option without a `value`
158
+ // attribute contributes its trimmed text. Reading IDL `.value`
159
+ // here keeps the template's indentation and loses the selection
160
+ // right after hydration.
161
+ const optionValue = typeof opt.hasAttribute === 'function' && opt.hasAttribute('value')
162
+ ? String(opt.getAttribute('value') ?? '')
163
+ : String(opt.textContent ?? opt.value ?? '').trim();
164
+ selections.push([opt, listed ? listed.includes(optionValue) : optionValue === next]);
165
+ }
166
+ for (const [opt, on] of selections)
167
+ opt.selected = on;
168
+ if (seed) {
169
+ for (const [opt, on] of selections) {
170
+ if (on)
171
+ opt.setAttribute('selected', '');
172
+ else if (typeof opt.removeAttribute === 'function')
173
+ opt.removeAttribute('selected');
174
+ }
65
175
  }
66
176
  }
67
177
  else if (el.value !== next) {
68
178
  el.value = next;
69
179
  }
180
+ if (el.tagName === 'TEXTAREA' && !seededTextareaDefaults.has(el)) {
181
+ // SSR ships a textarea's initial value as its text child. Resume parks
182
+ // that child in the adopt pool, so `defaultValue` (the form-reset target)
183
+ // stayed empty. Seed it on the first write; later model updates leave it.
184
+ seededTextareaDefaults.add(el);
185
+ el.defaultValue = next;
186
+ }
187
+ if (el.tagName === 'INPUT') {
188
+ // For text-like inputs the `value` attribute *is* the default value:
189
+ // seed it once, then let the IDL follow the model alone. Checkbox and
190
+ // radio `value` attributes are submitted values, not defaults.
191
+ if (isCheckableInput(el)) {
192
+ if (value == null || value === false)
193
+ el.removeAttribute('value');
194
+ else
195
+ el.setAttribute('value', next);
196
+ }
197
+ else if (!seededInputValueDefaults.has(el)) {
198
+ seededInputValueDefaults.add(el);
199
+ if (value == null || value === false)
200
+ el.removeAttribute('value');
201
+ else
202
+ el.setAttribute('value', next);
203
+ }
204
+ return;
205
+ }
206
+ // A select carries its value as option selectedness: a `value` attribute
207
+ // on the select itself has no meaning and SSR never emits one.
208
+ if (listed || el.tagName === 'SELECT')
209
+ return;
70
210
  if (value == null || value === false)
71
211
  el.removeAttribute('value');
72
212
  else
@@ -1,5 +1,5 @@
1
1
  import type { BindingId, DirectInstance, PatchFn } from '../direct-api.types.js';
2
- import type { EachCtx, ResumeAdoptCtx } from '../dom-core.types.js';
2
+ import type { EachCtx, ResumeAdoptCtx, VmzDomNode } from '../dom-core.types.js';
3
3
  export declare const directApi: {
4
4
  _inst: DirectInstance | null;
5
5
  _branchBinds: Array<{
@@ -10,14 +10,31 @@ export declare const directApi: {
10
10
  _itemPatches: PatchFn[] | null;
11
11
  _eachCtx: EachCtx | null;
12
12
  _resumeAdopt: ResumeAdoptCtx | null;
13
- el(tag: any): any;
13
+ el(tag: any, key?: any): any;
14
14
  elNS(ns: any, tag: any): Element;
15
15
  text(value: any): Text;
16
+ /**
17
+ * Emitted `v-html` write. Resume parks the SSR children of a reclaimed host,
18
+ * so the live `innerHTML` is still empty when the first patch runs. SSR stamps
19
+ * `HTML_HASH_ATTR` with the digest of the exact payload string it wrote: a
20
+ * matching digest proves the parked subtree is the same payload, so adopt
21
+ * moves it back and no text is ever re-parsed. A different digest writes the
22
+ * new payload and refreshes the marker for later identical runs.
23
+ */
24
+ html(el: any, value: any): void;
16
25
  nsScopeDepth(): number;
17
26
  rewindNsScope(depth: any): void;
18
27
  adoptEnter(node: any): boolean;
28
+ adoptEnterSlot(hostEl: any, name: any): boolean;
19
29
  adoptLeave(): void;
20
30
  frag(): DocumentFragment;
31
+ /**
32
+ * Multi-root `<template v-for>` row. Resume narrows adoption to the SSR
33
+ * marker pair for this key; a key with no pair gets an empty scope so a new
34
+ * row can never steal nodes that belong to another key.
35
+ */
36
+ fragRow(key?: any): DocumentFragment;
37
+ rowEnd(): void;
21
38
  comment(value: any): Comment;
22
39
  insertBefore(parent: any, node: any, ref: any): void;
23
40
  removeNode(node: any): void;
@@ -68,7 +85,14 @@ export declare const directApi: {
68
85
  /**
69
86
  * Nested component (sync Direct child or island schedule).
70
87
  */
71
- component(hostInst: any, nameOrCtor: any, props: any, client: any): any;
88
+ component(hostInst: any, nameOrCtor: any, props: any, client: any, key?: any): VmzDomNode;
89
+ /**
90
+ * `<component :is>` boundary. The host element is stable, the child instance
91
+ * is torn down and rebuilt whenever the evaluated `is` value changes. Props
92
+ * are re-evaluated per mount so the new type starts from current values, and
93
+ * the child listener bag is carried over so `@event` handlers keep working.
94
+ */
95
+ dynamicComponent(hostInst: any, bindingId: any, deps: any, getCtor: any, getProps: any, key?: any): any;
72
96
  /**
73
97
  * Subscribe to a child component event (`@submit` ??event name `submit`).
74
98
  * Orthogonal to function props (`:on-submit` ??prop `onSubmit`).
@@ -78,6 +102,30 @@ export declare const directApi: {
78
102
  * Keep nested Direct child props live with parent field writes.
79
103
  */
80
104
  bindComponentProp(hostInst: any, hostEl: any, propName: any, deps: any, get: any): void;
105
+ /**
106
+ * Object form of `bindComponentProp`: spread every key of the patch-time object
107
+ * across the child props and clear props whose keys disappear from the object.
108
+ */
109
+ bindComponentProps(hostInst: any, hostEl: any, slot: any, deps: any, get: any): void;
110
+ /**
111
+ * Object form of `onComponentEvent`: diff event names per call site and detach
112
+ * listeners whose keys disappear. No global `off` semantics — the bag entry
113
+ * belongs to this call site only.
114
+ */
115
+ bindComponentEvents(hostInst: any, hostEl: any, slot: any, deps: any, get: any): void;
116
+ /**
117
+ * `v-bind="obj"` / `v-on="obj"` listeners scoped to one call site.
118
+ *
119
+ * `mode: 'bind'` reads `onXxx` keys out of a bind object, `mode: 'on'` reads
120
+ * event names directly. Vue lowercases element listener names, so both modes
121
+ * normalize before touching `addEventListener`.
122
+ */
123
+ bindObjectListeners(el: any, slot: any, source: any, inst: any, mode: any): void;
124
+ /**
125
+ * Filter a `v-bind="obj"` source down to bindable keys before it reaches
126
+ * component props (`#slot` markers belong to the compiler, not the child).
127
+ */
128
+ spreadProps(source: any): Record<string, unknown>;
81
129
  /**
82
130
  * Project parent children into nested Direct component default `<slot>`.
83
131
  * Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.