@vmz/core 0.2.0 → 0.2.2

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.
@@ -2,11 +2,6 @@
2
2
  * Direct DOM API types for `@vmz/core` browser runtime.
3
3
  */
4
4
  export type BindingId = number | string | null;
5
- export type DirectInstance = Record<string, unknown> & {
6
- __vmzDestroyed?: boolean;
7
- __vmzDomRoot?: Node | null;
8
- __vmzFlushTrie?: Record<string, unknown> | null;
9
- };
10
5
  export type PatchFn = (this: DirectInstance) => void;
11
6
  export type DirectBranchCreate = (api: DirectApi) => Node;
12
7
  export type DirectIfBranch = {
@@ -20,6 +15,96 @@ export type DirectEachSpec = {
20
15
  createItem: DirectBranchCreate;
21
16
  serializeItem?: DirectBranchCreate;
22
17
  };
18
+ export type BinderEntry = {
19
+ deps: string[];
20
+ fn: PatchFn;
21
+ bindingId?: BindingId;
22
+ };
23
+ export type DirtyTrieNode = {
24
+ [seg: string]: DirtyTrieNode | true | undefined;
25
+ };
26
+ export type ComponentListenerBag = Record<string, Array<(detail?: unknown) => void>>;
27
+ export type VmzPlan = {
28
+ schema?: string;
29
+ root_ids?: Array<string | number>;
30
+ [key: string]: unknown;
31
+ };
32
+ export type TaskStatus = 'pending' | 'success' | 'error' | 'cancelled';
33
+ export type TaskEntry = {
34
+ generation: number;
35
+ controller: AbortController | {
36
+ signal: {
37
+ aborted: boolean;
38
+ };
39
+ abort: () => void;
40
+ };
41
+ status: TaskStatus;
42
+ result?: unknown;
43
+ error?: unknown;
44
+ promise?: Promise<unknown>;
45
+ };
46
+ export type DirtyNotice = {
47
+ type: 'replace';
48
+ root: string;
49
+ } | {
50
+ type: 'path';
51
+ root: string;
52
+ segs: string[];
53
+ };
54
+ /** Runtime instance shape mounted by Direct create / hydrate / resume. */
55
+ export type DirectInstance = {
56
+ [key: string]: unknown;
57
+ __vmzDestroyed?: boolean;
58
+ __vmzDomRoot?: (Node & {
59
+ __vmzInst?: DirectInstance | null;
60
+ }) | null;
61
+ __vmzFlushTrie?: DirtyTrieNode | null;
62
+ __vmzFlushScheduled?: boolean;
63
+ __vmzFlushSync?: boolean;
64
+ __vmzDirtyTrie?: DirtyTrieNode | null;
65
+ __vmzDirty?: Set<string>;
66
+ __vmzDirtyNotices?: DirtyNotice[];
67
+ __vmzLeafDirty?: Array<{
68
+ root: string;
69
+ field: string;
70
+ idxs: number[];
71
+ }>;
72
+ __vmzDrainLeafDirty?: boolean;
73
+ __vmzBinders?: Record<string, BinderEntry | PatchFn>;
74
+ __vmzBindings?: Record<string, BinderEntry | PatchFn>;
75
+ __vmzDepToBindings?: Record<string, Array<string | number>>;
76
+ __vmzPendingChildMounts?: Array<Promise<unknown>>;
77
+ __vmzComponentListeners?: ComponentListenerBag;
78
+ __vmzOnParentProp?: (propName: string, value: unknown) => void;
79
+ __vmzApplyProps?: (props: object) => void;
80
+ __vmzTasks?: Record<string, TaskEntry>;
81
+ __vmzPlan?: VmzPlan | null;
82
+ __vmzCtor?: ComponentCtorLike | null;
83
+ __vmzQuiet?: boolean;
84
+ __vmzEventDepth?: number;
85
+ __vmzEachApplyLeaf?: unknown;
86
+ __vmzEachCompoundStride?: unknown;
87
+ __vmzEachTranspose?: unknown;
88
+ onMount?: () => void | Promise<void>;
89
+ onDestroy?: () => void | Promise<void>;
90
+ };
91
+ export type ComponentCtorLike = (new (props?: object) => DirectInstance) & {
92
+ __vmzDirect?: boolean;
93
+ __vmzCreate?: (this: DirectInstance, api: DirectApi) => Node;
94
+ __vmzSerialize?: (this: DirectInstance, api: DirectApi) => unknown;
95
+ __vmzPlan?: VmzPlan | unknown;
96
+ __vmzHostBox?: string;
97
+ __vmzTag?: string;
98
+ __vmzState?: string[];
99
+ __vmzProps?: string[];
100
+ __vmzWBInstalled?: boolean;
101
+ __vmzCtorAppliesProps?: boolean;
102
+ name?: string;
103
+ };
104
+ export type DirectEachCtx = {
105
+ noteItemBind: (bindingId: BindingId, deps: string[], fn: PatchFn) => void;
106
+ needDelegate: (type: string) => void;
107
+ };
23
108
  /** Browser + generated artifact platform surface (0.2.0 thin runtime). */
24
109
  export type DirectApi = {
25
110
  _inst: DirectInstance | null;
@@ -29,16 +114,15 @@ export type DirectApi = {
29
114
  bindingId?: BindingId;
30
115
  }> | null;
31
116
  _itemPatches: PatchFn[] | null;
32
- _eachCtx: null | {
33
- noteItemBind: (bindingId: BindingId, deps: string[], fn: PatchFn) => void;
34
- needDelegate: (type: string) => void;
35
- };
117
+ _eachCtx: DirectEachCtx | null;
36
118
  _resumeAdopt: null | Record<string, unknown>;
37
119
  el: (tag: string) => Element;
38
120
  text: (value?: unknown) => Text;
39
121
  frag: () => DocumentFragment;
40
122
  comment: (value?: string) => Comment;
41
123
  attr: (el: Element, name: string, value: unknown) => void;
124
+ mergeClass: (...parts: unknown[]) => string;
125
+ mergeStyle: (...parts: unknown[]) => string;
42
126
  insertBefore: (parent: Node, node: Node, ref: Node | null) => void;
43
127
  removeNode: (node: Node) => void;
44
128
  trackPatch: (inst: DirectInstance, deps: string[], patch: PatchFn, bindingId?: BindingId) => void;
@@ -53,6 +137,5 @@ export type DirectApi = {
53
137
  setHtml: (el: Element, value: unknown) => void;
54
138
  bindHtml: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, el: Element) => void;
55
139
  bindComponentProp: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, hostInst: DirectInstance, propName: string) => void;
56
- component: (Ctor: new (props?: object) => DirectInstance, props?: object) => HTMLElement;
57
- [key: string]: unknown;
140
+ component: (Ctor: ComponentCtorLike | string, props?: object) => HTMLElement;
58
141
  };
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { BindingId, DirectInstance, PatchFn } from './direct-api.types.js';
10
10
  import type { ComponentCtor, EachCtx, ResumeAdoptCtx } from './dom-core.types.js';
11
+ export { mergeClassParts, mergeStyleParts } from '../shared/dom-attr-normalize.js';
11
12
  export { applyDirectHostBox, INLINE_HOST_CONTENTS, resolveDirectHostBox } from './direct-host-box.js';
12
13
  export { createUnknownComponentElement, markUnknownComponentHost, serializeUnknownComponentNode, UNKNOWN_COMPONENT_ERROR, } from './unknown-component.js';
13
14
  export declare function __vmzPrecisionEnable(on?: boolean): void;
@@ -80,6 +81,8 @@ export declare function runDirectCreate(Component: any, inst: any): any;
80
81
  * projects parent siblings into the wrong host (commercial Drawer into Create btn).
81
82
  */
82
83
  export declare function findOwnedDefaultSlot(root: any): any;
84
+ /** Button/Link shell when `<slot>` is absent (SSR-flattened or resume adopt). */
85
+ export declare function findOwnedDefaultSlotTarget(root: any): any;
83
86
  export declare const directApi: {
84
87
  _inst: DirectInstance | null;
85
88
  _branchBinds: Array<{
@@ -91,6 +94,7 @@ export declare const directApi: {
91
94
  _eachCtx: EachCtx | null;
92
95
  _resumeAdopt: ResumeAdoptCtx | null;
93
96
  el(tag: any): any;
97
+ elNS(ns: any, tag: any): Element;
94
98
  text(value: any): Text;
95
99
  adoptEnter(node: any): boolean;
96
100
  adoptLeave(): void;
@@ -105,6 +109,8 @@ export declare const directApi: {
105
109
  untrackPatch(inst: any, deps: any, patch: any, bindingId?: any): void;
106
110
  disposeTree(root: any): void;
107
111
  attr(el: any, name: any, value: any): void;
112
+ mergeClass(...parts: any[]): string;
113
+ mergeStyle(...parts: any[]): string;
108
114
  on(el: any, type: any, handler: any): void;
109
115
  /**
110
116
  * Bind a named instance method (no arrow / Function#toString).
@@ -6,8 +6,10 @@
6
6
  * Mount and client patches run that same schedule (SSR/hydrate/resume in dom-ssr).
7
7
  * Field writes only run registered dep patches ??never re-create structure.
8
8
  */
9
+ import { mergeClassParts, mergeStyleParts } from '../shared/dom-attr-normalize.js';
9
10
  import { applyDirectHostBox } from './direct-host-box.js';
10
11
  import { createUnknownComponentElement } from './unknown-component.js';
12
+ export { mergeClassParts, mergeStyleParts } from '../shared/dom-attr-normalize.js';
11
13
  export { applyDirectHostBox, INLINE_HOST_CONTENTS, resolveDirectHostBox } from './direct-host-box.js';
12
14
  export { createUnknownComponentElement, markUnknownComponentHost, serializeUnknownComponentNode, UNKNOWN_COMPONENT_ERROR, } from './unknown-component.js';
13
15
  const components = Object.create(null);
@@ -148,7 +150,7 @@ export function getRegisteredComponent(name) {
148
150
  export async function resolveComponent(name) {
149
151
  let Ctor = components[name];
150
152
  if (!Ctor && typeof globalThis.__vmzLoadComponent === 'function') {
151
- Ctor = await globalThis.__vmzLoadComponent(name);
153
+ Ctor = (await globalThis.__vmzLoadComponent(name));
152
154
  if (Ctor)
153
155
  registerComponents({ [name]: Ctor });
154
156
  }
@@ -260,6 +262,33 @@ export function findOwnedDefaultSlot(root) {
260
262
  }
261
263
  return null;
262
264
  }
265
+ /** Button/Link shell when `<slot>` is absent (SSR-flattened or resume adopt). */
266
+ export function findOwnedDefaultSlotTarget(root) {
267
+ const slot = findOwnedDefaultSlot(root);
268
+ if (slot)
269
+ return slot;
270
+ if (!root || root.nodeType !== 1)
271
+ return null;
272
+ const walk = (el) => {
273
+ if (!el || el.nodeType !== 1)
274
+ return null;
275
+ const tag = String(el.tagName || '').toLowerCase();
276
+ if ((tag === 'button' || tag === 'a') && el.getAttribute('data-vmz-ui') === 'button')
277
+ return el;
278
+ if (el.hasAttribute('data-vmz'))
279
+ return null;
280
+ const kids = el.children;
281
+ if (!kids || !kids.length)
282
+ return null;
283
+ for (let i = 0; i < kids.length; i++) {
284
+ const hit = walk(kids[i]);
285
+ if (hit)
286
+ return hit;
287
+ }
288
+ return null;
289
+ };
290
+ return walk(root);
291
+ }
263
292
  export const directApi = {
264
293
  _inst: null,
265
294
  _branchBinds: null,
@@ -272,6 +301,13 @@ export const directApi = {
272
301
  noteDomCreate();
273
302
  return document.createElement(tag || 'div');
274
303
  },
304
+ elNS(ns, tag) {
305
+ if (directApi._resumeAdopt && typeof directApi._resumeAdopt.elNS === 'function') {
306
+ return directApi._resumeAdopt.elNS(ns, tag);
307
+ }
308
+ noteDomCreate();
309
+ return document.createElementNS(ns || 'http://www.w3.org/2000/svg', tag || 'g');
310
+ },
275
311
  text(value) {
276
312
  if (directApi._resumeAdopt)
277
313
  return directApi._resumeAdopt.text(value);
@@ -374,6 +410,12 @@ export const directApi = {
374
410
  attr(el, name, value) {
375
411
  applyDomAttr(el, name, value);
376
412
  },
413
+ mergeClass(...parts) {
414
+ return mergeClassParts(...parts);
415
+ },
416
+ mergeStyle(...parts) {
417
+ return mergeStyleParts(...parts);
418
+ },
377
419
  on(el, type, handler) {
378
420
  const inst = directApi._inst;
379
421
  if (directApi._eachCtx && typeof handler === 'function') {
@@ -479,12 +521,19 @@ export const directApi = {
479
521
  directApi.trackPatch(inst, [fieldName], function specFieldAttrPatch() {
480
522
  const raw = this[fieldName];
481
523
  if (name === 'class' || name === 'className') {
482
- const s = String(raw ?? '');
524
+ const s = mergeClassParts(raw);
483
525
  if (s)
484
526
  el.setAttribute('class', s);
485
527
  else if (el.hasAttribute('class'))
486
528
  el.removeAttribute('class');
487
529
  }
530
+ else if (name === 'style') {
531
+ const s = mergeStyleParts(raw);
532
+ if (s)
533
+ el.setAttribute('style', s);
534
+ else if (el.hasAttribute('style'))
535
+ el.removeAttribute('style');
536
+ }
488
537
  else {
489
538
  applyDomAttr(el, name, raw);
490
539
  }
@@ -662,12 +711,13 @@ export const directApi = {
662
711
  // Emptied DocumentFragment after append must not receive slot kids.
663
712
  if (!root || root.nodeType !== 1)
664
713
  root = hostEl;
665
- let slot = null;
666
- if (root && root.nodeType === 1) {
667
- slot = findOwnedDefaultSlot(root);
668
- }
669
- if (slot && slot.parentNode) {
670
- slot.replaceWith(node);
714
+ const target = root && root.nodeType === 1 ? findOwnedDefaultSlotTarget(root) : null;
715
+ if (target && target.parentNode) {
716
+ if (String(target.tagName || '').toLowerCase() === 'slot') {
717
+ target.replaceWith(node);
718
+ return;
719
+ }
720
+ target.appendChild(node);
671
721
  return;
672
722
  }
673
723
  if (root && root.nodeType === 1 && typeof root.appendChild === 'function')
@@ -731,6 +781,22 @@ export const BOOLEAN_HTML_ATTRS = new Set([
731
781
  ]);
732
782
  export function applyDomAttr(el, name, value) {
733
783
  const key = name === 'className' ? 'class' : name;
784
+ if (key === 'class') {
785
+ const s = mergeClassParts(value);
786
+ if (s)
787
+ el.setAttribute('class', s);
788
+ else
789
+ el.removeAttribute('class');
790
+ return;
791
+ }
792
+ if (key === 'style') {
793
+ const s = mergeStyleParts(value);
794
+ if (s)
795
+ el.setAttribute('style', s);
796
+ else
797
+ el.removeAttribute('style');
798
+ return;
799
+ }
734
800
  if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
735
801
  if (value === false || value == null || value === '') {
736
802
  el.removeAttribute(key);
@@ -1,20 +1,9 @@
1
1
  /**
2
2
  * Internal types for `@vmz/core` browser DOM runtime (`dom-core.ts`).
3
3
  */
4
- import type { DirectApi, DirectInstance, PatchFn, BindingId } from './direct-api.types.js';
5
- export type { DirectApi, DirectInstance, PatchFn, BindingId };
6
- export type ComponentCtor = (new (props?: object) => DirectInstance) & {
7
- __vmzDirect?: boolean;
8
- __vmzCreate?: (this: DirectInstance, api: DirectApi) => Node;
9
- __vmzSerialize?: (this: DirectInstance, api: DirectApi) => unknown;
10
- __vmzPlan?: unknown;
11
- __vmzHostBox?: string;
12
- __vmzTag?: string;
13
- __vmzState?: string[];
14
- __vmzProps?: string[];
15
- __vmzWBInstalled?: boolean;
16
- __vmzCtorAppliesProps?: boolean;
17
- name?: string;
4
+ import type { BindingId, ComponentCtorLike, DirectApi, DirectInstance, PatchFn, VmzPlan } from './direct-api.types.js';
5
+ export type { BindingId, DirectApi, DirectInstance, PatchFn, VmzPlan };
6
+ export type ComponentCtor = ComponentCtorLike & {
18
7
  [key: string]: unknown;
19
8
  };
20
9
  export type VmzDomElement = Element & {
@@ -81,6 +70,7 @@ export type TraceBuffer = {
81
70
  };
82
71
  export type ResumeAdoptCtx = {
83
72
  el: (tag: string) => Element;
73
+ elNS?: (ns: string, tag: string) => Element;
84
74
  text: (value?: unknown) => Text;
85
75
  componentHost?: (name: string) => HTMLElement | null;
86
76
  enter?: (node: Element) => boolean;
@@ -35,8 +35,7 @@ export function noteUnknownComponent(name, via = 'ssr') {
35
35
  if (!Array.isArray(g.__VMZ_COMPONENT_ERRORS__)) {
36
36
  g.__VMZ_COMPONENT_ERRORS__ = [];
37
37
  }
38
- const bag = g.__VMZ_COMPONENT_ERRORS__;
39
- bag.push(detail);
38
+ g.__VMZ_COMPONENT_ERRORS__.push(detail);
40
39
  }
41
40
  catch {
42
41
  /* ignore */
@@ -4,4 +4,4 @@
4
4
  * Full barrel remains `@vmz/core/dom` (`dom.js` / dist `vmz-dom.js`) for Node host.
5
5
  */
6
6
  export * from '../browser/dom-core.js';
7
- export { attachEventEntries, hydrate, hydrateIslands, hydrateRoute, hydrateRoutePage, resume, resumeIslands, } from '../ssr/dom-ssr.js';
7
+ export { attachEventEntries, hydrate, hydrateIslands, hydrateRoute, hydrateRoutePage, resume, resumeIslands } from '../ssr/dom-ssr.js';
@@ -4,4 +4,4 @@
4
4
  * Full barrel remains `@vmz/core/dom` (`dom.js` / dist `vmz-dom.js`) for Node host.
5
5
  */
6
6
  export * from '../browser/dom-core.js';
7
- export { attachEventEntries, hydrate, hydrateIslands, hydrateRoute, hydrateRoutePage, resume, resumeIslands, } from '../ssr/dom-ssr.js';
7
+ export { attachEventEntries, hydrate, hydrateIslands, hydrateRoute, hydrateRoutePage, resume, resumeIslands } from '../ssr/dom-ssr.js';
@@ -1,6 +1,6 @@
1
- /** Compile-time HTTP decorators �?erased from server JS emit; kept for authoring. */
2
- export declare function Get(path: any): (_target: any, _key: any, descriptor: any) => any;
3
- export declare function Post(path: any): (_target: any, _key: any, descriptor: any) => any;
4
- export declare function Put(path: any): (_target: any, _key: any, descriptor: any) => any;
5
- export declare function Delete(path: any): (_target: any, _key: any, descriptor: any) => any;
6
- export declare function Patch(path: any): (_target: any, _key: any, descriptor: any) => any;
1
+ /** Compile-time HTTP decorators — erased from server JS emit; kept for authoring. */
2
+ export declare function Get(path: string): (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
3
+ export declare function Post(path: string): (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
4
+ export declare function Put(path: string): (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
5
+ export declare function Delete(path: string): (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
6
+ export declare function Patch(path: string): (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
@@ -1,4 +1,4 @@
1
- /** Compile-time HTTP decorators �?erased from server JS emit; kept for authoring. */
1
+ /** Compile-time HTTP decorators — erased from server JS emit; kept for authoring. */
2
2
  export function Get(path) {
3
3
  return function GetDecorator(_target, _key, descriptor) {
4
4
  return descriptor;
@@ -4,26 +4,27 @@
4
4
  * Browser-safe: no static `node:*` imports. Node builtins are loaded only inside
5
5
  * Node/SSR request handlers so client bundles can `import { callServer }`.
6
6
  */
7
- import type { Route, ServerModuleResolver } from '../shared/server.types.js';
7
+ import type { IncomingMessage, ServerResponse } from 'node:http';
8
+ import type { NodeRequestOptions, Route, RpcRequest, ServerModuleResolver } from '../shared/server.types.js';
8
9
  /**
9
- * Map `#server/foo` �?filesystem / URL the host can `import`.
10
- * Only set this in Node/SSR hosts �?browser bundles must omit it so RPC goes HTTP.
10
+ * Map `#server/foo` → filesystem / URL the host can `import`.
11
+ * Only set this in Node/SSR hosts → browser bundles must omit it so RPC goes HTTP.
11
12
  */
12
13
  export declare function setServerModuleResolver(fn: ServerModuleResolver): void;
13
14
  export declare function setRoutes(next: Route[]): void;
14
15
  export declare function callServer(moduleId: string, method: string, args?: unknown[]): Promise<unknown>;
15
- export declare function handleRpc(body: any): Promise<any>;
16
+ export declare function handleRpc(body: RpcRequest): Promise<unknown>;
16
17
  /**
17
18
  * Match REST route from `vmz-routes.json`.
18
19
  */
19
- export declare function matchRoute(verb: any, pathname: any): Route;
20
+ export declare function matchRoute(verb: string, pathname: string): Route | null;
20
21
  /**
21
22
  * Web Standards Fetch entry for ServerArtifact hosts (Node adapter, worker/edge parity).
22
23
  * Handles RPC + public ServerRoute only; static/SSR stay on Node host options.
23
24
  */
24
- export declare function handleFetchRequest(request: any): Promise<Response>;
25
+ export declare function handleFetchRequest(request: Request): Promise<Response>;
25
26
  /**
26
27
  * Node `http.createServer` listener: RPC + REST + optional static / SSR index.
27
28
  * RPC/REST go through {@link handleFetchRequest} so Node and Fetch hosts share one core.
28
29
  */
29
- export declare function handleNodeRequest(req: any, res: any, opts?: any): Promise<void>;
30
+ export declare function handleNodeRequest(req: IncomingMessage, res: ServerResponse, opts?: NodeRequestOptions): Promise<void>;
@@ -8,8 +8,8 @@ const DEFAULT_RPC_PATH = '/__vmz/rpc';
8
8
  let resolveServerModule = null;
9
9
  let routes = [];
10
10
  /**
11
- * Map `#server/foo` �?filesystem / URL the host can `import`.
12
- * Only set this in Node/SSR hosts �?browser bundles must omit it so RPC goes HTTP.
11
+ * Map `#server/foo` → filesystem / URL the host can `import`.
12
+ * Only set this in Node/SSR hosts → browser bundles must omit it so RPC goes HTTP.
13
13
  */
14
14
  export function setServerModuleResolver(fn) {
15
15
  resolveServerModule = fn;
@@ -19,7 +19,7 @@ export function setRoutes(next) {
19
19
  }
20
20
  export async function callServer(moduleId, method, args = []) {
21
21
  // Browser (or tests forcing HTTP): never touch server modules in-process.
22
- // Node SSR / smokes set a resolver and leave __VMZ_USE_HTTP_RPC unset �?local.
22
+ // Node SSR / smokes set a resolver and leave __VMZ_USE_HTTP_RPC unset → local.
23
23
  if (globalThis.__VMZ_USE_HTTP_RPC || !resolveServerModule) {
24
24
  return callServerHttp(moduleId, method, args);
25
25
  }
@@ -30,11 +30,12 @@ async function callServerLocal(moduleId, method, args) {
30
30
  throw new Error(`vmz:runtime callServer(${moduleId}): setServerModuleResolver() required in Node`);
31
31
  }
32
32
  const spec = resolveServerModule(moduleId);
33
- const mod = await import(typeof spec === 'string' ? spec : spec.href);
34
- const Ctor = mod.default ?? mod[exportGuess(moduleId)];
35
- if (typeof Ctor !== 'function') {
33
+ const mod = (await import(typeof spec === 'string' ? spec : spec.href));
34
+ const exported = mod.default ?? mod[exportGuess(moduleId)];
35
+ if (typeof exported !== 'function') {
36
36
  throw new Error(`vmz:runtime: no class export for ${moduleId}`);
37
37
  }
38
+ const Ctor = exported;
38
39
  const instance = new Ctor();
39
40
  const fn = instance[method];
40
41
  if (typeof fn !== 'function') {
@@ -84,8 +85,8 @@ export function matchRoute(verb, pathname) {
84
85
  }
85
86
  /**
86
87
  * Build `callServerLocal` args from a Fetch Request for REST routes.
87
- * GET/HEAD �?`[]`. JSON POST/PUT/PATCH �?`[body]`. form-urlencoded �?`[record]`.
88
- * Multipart �?`[record]` where File/Blob parts stay as File/Blob (tool-site binary upload).
88
+ * GET/HEAD → `[]`. JSON POST/PUT/PATCH → `[body]`. form-urlencoded → `[record]`.
89
+ * Multipart → `[record]` where File/Blob parts stay as File/Blob (tool-site binary upload).
89
90
  * Octet-stream PUT also forwards Upload resumable chunk headers (upload-id / chunk-index / chunk-total).
90
91
  * Extra args are ignored by zero-parameter server methods (JS).
91
92
  */
@@ -106,12 +107,12 @@ async function routeArgsFromRequest(request, verb) {
106
107
  }
107
108
  }
108
109
  if (ctype.includes('multipart/form-data')) {
109
- // Parse from raw bytes �?undici Request.formData() can UTF-8-mangle high bytes in file parts
110
- // (0xFF/0xFE �?U+FFFD), which breaks Upload binary / tool-site intakes.
110
+ // Parse from raw bytes → undici Request.formData() can UTF-8-mangle high bytes in file parts
111
+ // (0xFF/0xFE → U+FFFD), which breaks Upload binary / tool-site intakes.
111
112
  const buf = Buffer.from(await request.arrayBuffer());
112
113
  return [parseMultipartBuffer(buf, ctype)];
113
114
  }
114
- // Object-store PUT / resumable chunk PUT �?keep bytes (never request.text()).
115
+ // Object-store PUT / resumable chunk PUT → keep bytes (never request.text()).
115
116
  if (ctype.includes('application/octet-stream') ||
116
117
  ((v === 'PUT' || v === 'PATCH') && !ctype.includes('json') && !ctype.includes('x-www-form-urlencoded'))) {
117
118
  const buf = Buffer.from(await request.arrayBuffer());
@@ -145,7 +146,7 @@ export async function handleFetchRequest(request) {
145
146
  const verb = (request.method || 'GET').toUpperCase();
146
147
  try {
147
148
  if (verb === 'POST' && url.pathname === DEFAULT_RPC_PATH) {
148
- const body = await request.json();
149
+ const body = (await request.json());
149
150
  const result = await handleRpc(body);
150
151
  return Response.json(result);
151
152
  }
@@ -179,10 +180,11 @@ export async function handleNodeRequest(req, res, opts = {}) {
179
180
  const response = await handleFetchRequest(request);
180
181
  return await writeFetchResponse(res, response);
181
182
  }
182
- // Static first for assets + DocumentMount (`/d/�`) so docs aren't swallowed by SSR 404 shells.
183
- // static route HTML (`index.html`, `about/index.html`, �? is a CDN/deploy projection only �? // when Server Host SSR is active, those files must not shadow live render (local/dev �?SSR truth).
183
+ // Static first for assets + DocumentMount (`/d/…`) so docs aren't swallowed by SSR 404 shells.
184
+ // static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
185
+ // when Server Host SSR is active, those files must not shadow live render (local/dev → SSR truth).
184
186
  if (verb === 'GET' && opts.distDir) {
185
- const nodePath = await import('node:path');
187
+ const nodePath = (await import('node:path'));
186
188
  const { readFile, stat } = await import('node:fs/promises');
187
189
  const file = await resolveDistStatic(opts.distDir, url.pathname, nodePath, stat, {
188
190
  cookieHeader: String(req.headers.cookie || ''),
@@ -197,7 +199,8 @@ export async function handleNodeRequest(req, res, opts = {}) {
197
199
  return sendBytes(res, 200, body, contentType(file, nodePath));
198
200
  }
199
201
  catch (err) {
200
- if (err && (err.code === 'ENOENT' || err.code === 'EISDIR')) {
202
+ const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
203
+ if (code === 'ENOENT' || code === 'EISDIR') {
201
204
  /* fall through */
202
205
  }
203
206
  else {
@@ -216,7 +219,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
216
219
  /* ignore */
217
220
  }
218
221
  };
219
- // Do not abort on IncomingMessage `close` �?that fires after a POST body is
222
+ // Do not abort on IncomingMessage `close` — that fires after a POST body is
220
223
  // fully read (normal), which would cancel SSR before the first chunk.
221
224
  req.on('aborted', onClientGone);
222
225
  res.on('close', () => {
@@ -244,7 +247,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
244
247
  body,
245
248
  });
246
249
  if (rendered) {
247
- if (typeof rendered === 'object' && rendered.redirect) {
250
+ if (typeof rendered === 'object' && 'redirect' in rendered && rendered.redirect) {
248
251
  const status = Number(rendered.status) || 302;
249
252
  const headers = {
250
253
  Location: String(rendered.redirect),
@@ -255,7 +258,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
255
258
  return;
256
259
  }
257
260
  const status = rendered && typeof rendered === 'object' && 'status' in rendered ? Number(rendered.status) || 200 : 200;
258
- const stream = rendered && typeof rendered === 'object' && rendered.stream ? rendered.stream : rendered;
261
+ const stream = rendered && typeof rendered === 'object' && 'stream' in rendered && rendered.stream ? rendered.stream : rendered;
259
262
  return await sendHtmlStream(res, status, stream, ac.signal);
260
263
  }
261
264
  }
@@ -355,7 +358,7 @@ function safeDistFile(distDir, pathname, nodePath) {
355
358
  }
356
359
  /**
357
360
  * static profile emits per-route HTML beside client assets. That HTML is for CDN / local-static
358
- * delivery hosts �?not for Server Host when SSR is available. DocumentMount stays static.
361
+ * delivery hosts — not for Server Host when SSR is available. DocumentMount stays static.
359
362
  */
360
363
  function isWebStaticHtmlShadow(file, pathname, nodePath) {
361
364
  const ext = nodePath.extname(file).toLowerCase();
@@ -364,14 +367,14 @@ function isWebStaticHtmlShadow(file, pathname, nodePath) {
364
367
  let rel = decodeURIComponent(String(pathname || '').split('?')[0] || '/');
365
368
  if (!rel.startsWith('/'))
366
369
  rel = `/${rel}`;
367
- // Integrated DocumentMount �?keep static-first (see resolveDistStatic comment).
370
+ // Integrated DocumentMount — keep static-first (see resolveDistStatic comment).
368
371
  if (rel === '/d' || rel.startsWith('/d/'))
369
372
  return false;
370
373
  return true;
371
374
  }
372
375
  /**
373
376
  * Static resolve for app assets + integrated document mounts:
374
- * `/d/` �?`d/index.html`, `/d/zh-hans/guide` �?`d/zh-hans/guide.html`.
377
+ * `/d/` → `d/index.html`, `/d/zh-hans/guide` → `d/zh-hans/guide.html`.
375
378
  */
376
379
  async function resolveDistStatic(distDir, pathname, nodePath, stat, _opts = {}) {
377
380
  const candidates = staticPathCandidates(pathname);
@@ -451,7 +454,9 @@ function contentType(filePath, nodePath) {
451
454
  function readJson(req) {
452
455
  return new Promise((resolve, reject) => {
453
456
  const chunks = [];
454
- req.on('data', (c) => chunks.push(c));
457
+ req.on('data', (c) => {
458
+ chunks.push(typeof c === 'string' ? Buffer.from(c) : c);
459
+ });
455
460
  req.on('end', () => {
456
461
  try {
457
462
  const raw = Buffer.concat(chunks).toString('utf8') || '{}';
@@ -467,8 +472,10 @@ function readJson(req) {
467
472
  function readRawBody(req) {
468
473
  return new Promise((resolve, reject) => {
469
474
  const chunks = [];
470
- req.on('data', (c) => chunks.push(c));
471
- // Buffer �?never utf8-string: multipart File bytes must survive (Upload binary gate).
475
+ req.on('data', (c) => {
476
+ chunks.push(typeof c === 'string' ? Buffer.from(c) : c);
477
+ });
478
+ // Buffer — never utf8-string: multipart File bytes must survive (Upload binary gate).
472
479
  req.on('end', () => resolve(Buffer.concat(chunks)));
473
480
  req.on('error', reject);
474
481
  });
@@ -513,7 +520,7 @@ function parseMultipartBuffer(buf, contentType) {
513
520
  if (fileM) {
514
521
  const filename = fileM[1] || 'upload.bin';
515
522
  const type = typeM ? String(typeM[1]).trim() : 'application/octet-stream';
516
- // Copy body �?File may outlive the request buffer.
523
+ // Copy body — File may outlive the request buffer.
517
524
  const copy = Buffer.from(body);
518
525
  const file = new File([copy], filename, { type });
519
526
  const prev = out[key];
@@ -551,19 +558,20 @@ function indexOfBuffer(hay, needle, from) {
551
558
  return -1;
552
559
  }
553
560
  function parseFormBody(raw, contentType) {
554
- if (!raw)
561
+ const text = typeof raw === 'string' ? raw : raw.toString('utf8');
562
+ if (!text)
555
563
  return {};
556
564
  if (contentType.includes('application/x-www-form-urlencoded')) {
557
565
  const out = {};
558
- for (const [k, v] of new URLSearchParams(raw))
566
+ for (const [k, v] of new URLSearchParams(text))
559
567
  out[k] = v;
560
568
  return out;
561
569
  }
562
570
  try {
563
- return JSON.parse(raw);
571
+ return JSON.parse(text);
564
572
  }
565
573
  catch {
566
- return { raw };
574
+ return { raw: text };
567
575
  }
568
576
  }
569
577
  function wantsBrowserHtml(req) {
@@ -592,7 +600,7 @@ function wantsBrowserHtml(req) {
592
600
  }
593
601
  /**
594
602
  * Browser-facing error document (no N-API). Used when SSR returns null or throws
595
- * on a document navigation �?must not fall back to API-shaped JSON.
603
+ * on a document navigation — must not fall back to API-shaped JSON.
596
604
  */
597
605
  function browserErrorHtml(status, title, message, stack) {
598
606
  const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
@@ -619,7 +627,7 @@ function browserErrorHtml(status, title, message, stack) {
619
627
  <h1>${esc(title)}</h1>
620
628
  <pre>${esc(message)}</pre>
621
629
  ${stackBlock}
622
- <p class="hint">Document navigation returns HTML errors �?not API JSON.</p>
630
+ <p class="hint">Document navigation returns HTML errors — not API JSON.</p>
623
631
  </main>
624
632
  </body>
625
633
  </html>
@@ -652,8 +660,8 @@ async function sendHtmlStream(res, status, source, signal) {
652
660
  'transfer-encoding': 'chunked',
653
661
  'cache-control': 'no-cache',
654
662
  };
655
- // Defer writeHead until the first successful chunk so SSR throw �?clean 500
656
- // (not headersSent + destroy �?ERR_EMPTY_RESPONSE).
663
+ // Defer writeHead until the first successful chunk so SSR throw → clean 500
664
+ // (not headersSent + destroy → ERR_EMPTY_RESPONSE).
657
665
  let started = false;
658
666
  try {
659
667
  for await (const chunk of source) {
@@ -691,7 +699,7 @@ async function sendHtmlStream(res, status, source, signal) {
691
699
  catch (err) {
692
700
  if (aborted())
693
701
  return;
694
- // Before headers: let outer handler sendJson(500). After headers: rethrow �?destroy.
702
+ // Before headers: let outer handler sendJson(500). After headers: rethrow → destroy.
695
703
  throw err;
696
704
  }
697
705
  if (started && !res.writableEnded && !res.destroyed) {
@@ -703,7 +711,7 @@ function sendBytes(res, status, body, type) {
703
711
  'content-type': type,
704
712
  'content-length': body.byteLength,
705
713
  };
706
- // Dev: stylesheets are rebuilt in-place �?never cache @import siblings (VMZ-8).
714
+ // Dev: stylesheets are rebuilt in-place — never cache @import siblings (VMZ-8).
707
715
  if (process.env.VMZ_DEV === '1' && typeof type === 'string' && type.startsWith('text/css')) {
708
716
  headers['cache-control'] = 'no-store';
709
717
  }
@@ -7,8 +7,8 @@ export declare function createRenderHost(distDir: string, opts?: Record<string,
7
7
  deployment: import("../shared/host.types.js").DeploymentDocument;
8
8
  dom: any;
9
9
  loadedChunkIds: Set<string>;
10
- ensureComponents: (rootChunkIds: any) => Promise<import("../shared/host.types.js").ComponentRegistryMap>;
11
- closureChunkIds: (rootChunkIds: any) => Set<unknown>;
10
+ ensureComponents: (rootChunkIds?: string[] | null) => Promise<import("../shared/host.types.js").ComponentRegistryMap>;
11
+ closureChunkIds: (rootChunkIds?: string[] | null) => Set<string>;
12
12
  renderToString: any;
13
13
  renderToStream: any;
14
14
  mount: any;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import path from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
- import { bootstrapComponentRegistry, collectDependsOnClosure, loadComponentEntries, readDeploymentDocument, importAndRegisterComponentEntries, } from './deployment-registry.js';
7
+ import { bootstrapComponentRegistry, collectDependsOnClosure, importAndRegisterComponentEntries, loadComponentEntries, readDeploymentDocument, } from './deployment-registry.js';
8
8
  export async function createRenderHost(distDir, opts = {}) {
9
9
  const strict = opts.strictDeployment === true || opts.strict === true;
10
10
  const domPath = path.join(distDir, 'vmz-dom.js');
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared class/style normalization for Direct SSR + client (0.1.33).
3
+ * Vue-compatible: string | string[] | object map | falsy parts.
4
+ */
5
+ /** Normalize one or more class fragments into a deduped class string. */
6
+ export declare function mergeClassParts(...parts: unknown[]): string;
7
+ /** Merge static + dynamic style fragments; later parts override same keys. */
8
+ export declare function mergeStyleParts(...parts: unknown[]): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared class/style normalization for Direct SSR + client (0.1.33).
3
+ * Vue-compatible: string | string[] | object map | falsy parts.
4
+ */
5
+ function pushClassTokens(out, value) {
6
+ if (value == null || value === false)
7
+ return;
8
+ if (typeof value === 'string') {
9
+ for (const token of value.split(/\s+/)) {
10
+ if (token)
11
+ out.push(token);
12
+ }
13
+ return;
14
+ }
15
+ if (Array.isArray(value)) {
16
+ for (const item of value)
17
+ pushClassTokens(out, item);
18
+ return;
19
+ }
20
+ if (typeof value === 'object') {
21
+ for (const [key, enabled] of Object.entries(value)) {
22
+ if (enabled)
23
+ out.push(key);
24
+ }
25
+ return;
26
+ }
27
+ const s = String(value).trim();
28
+ if (s)
29
+ out.push(s);
30
+ }
31
+ /** Normalize one or more class fragments into a deduped class string. */
32
+ export function mergeClassParts(...parts) {
33
+ const tokens = [];
34
+ for (const part of parts)
35
+ pushClassTokens(tokens, part);
36
+ return [...new Set(tokens)].join(' ');
37
+ }
38
+ function parseStyleObject(value) {
39
+ const out = {};
40
+ if (value == null || value === false)
41
+ return out;
42
+ if (typeof value === 'string') {
43
+ for (const chunk of value.split(';')) {
44
+ const idx = chunk.indexOf(':');
45
+ if (idx < 0)
46
+ continue;
47
+ const key = chunk.slice(0, idx).trim();
48
+ const val = chunk.slice(idx + 1).trim();
49
+ if (key)
50
+ out[key] = val;
51
+ }
52
+ return out;
53
+ }
54
+ if (typeof value === 'object' && !Array.isArray(value)) {
55
+ for (const [key, raw] of Object.entries(value)) {
56
+ if (raw == null || raw === false)
57
+ continue;
58
+ out[key] = String(raw);
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ /** Merge static + dynamic style fragments; later parts override same keys. */
64
+ export function mergeStyleParts(...parts) {
65
+ const merged = {};
66
+ for (const part of parts)
67
+ Object.assign(merged, parseStyleObject(part));
68
+ return Object.entries(merged)
69
+ .map(([key, val]) => `${key}: ${val}`)
70
+ .join('; ');
71
+ }
@@ -32,7 +32,28 @@ export type ClosedAccessResult = {
32
32
  kind: 'not-found';
33
33
  };
34
34
  export type SseClient = Pick<ServerResponse, 'write' | 'end'>;
35
- export type DeploymentDocument = Record<string, unknown>;
35
+ /** Deployment graph document (`vmz.deployment.v0`). */
36
+ export type DeploymentUnit = {
37
+ chunkId?: string;
38
+ name?: string;
39
+ entry?: string;
40
+ source?: string;
41
+ dependsOn?: string[];
42
+ [key: string]: unknown;
43
+ };
44
+ export type DeploymentLayoutChain = {
45
+ routeId?: string;
46
+ layouts?: string[];
47
+ page?: string;
48
+ [key: string]: unknown;
49
+ };
50
+ export type DeploymentDocument = {
51
+ schema?: string;
52
+ units?: DeploymentUnit[];
53
+ layoutChain?: DeploymentLayoutChain[] | Record<string, unknown>;
54
+ dependsOn?: Record<string, string[]>;
55
+ [key: string]: unknown;
56
+ };
36
57
  export type ComponentRegistryMap = Record<string, unknown>;
37
58
  export type LoadComponentEntriesOpts = {
38
59
  strict?: boolean;
@@ -65,3 +86,28 @@ export type PreloadComponentRegistryOpts = ListClientComponentsOpts & {
65
86
  preload?: 'all' | 'closure' | 'none';
66
87
  };
67
88
  export type NativeAddon = Record<string, unknown>;
89
+ export type ServeHostOpts = {
90
+ distDir?: string;
91
+ port?: number;
92
+ host?: string;
93
+ signal?: AbortSignal;
94
+ [key: string]: unknown;
95
+ };
96
+ export type SoftReloadOpts = {
97
+ reason?: string;
98
+ signal?: AbortSignal;
99
+ [key: string]: unknown;
100
+ };
101
+ export type RenderPageStreamOpts = HostRequestOpts & {
102
+ localeId?: string;
103
+ [key: string]: unknown;
104
+ };
105
+ export type RenderHostOpts = {
106
+ strict?: boolean;
107
+ closureRoots?: string[];
108
+ explicit?: Record<string, string>;
109
+ preload?: 'all' | 'closure' | 'none';
110
+ cacheBust?: string | number;
111
+ rootChunkIds?: string[] | null;
112
+ [key: string]: unknown;
113
+ };
@@ -37,3 +37,31 @@ export type NodeRequestOptions = {
37
37
  req?: IncomingMessage;
38
38
  };
39
39
  export type ServerModuleResolver = (id: string) => string | URL;
40
+ /** Subset of `node:path` used by static resolve helpers. */
41
+ export type NodePathModule = {
42
+ resolve(...paths: string[]): string;
43
+ join(...paths: string[]): string;
44
+ extname(path: string): string;
45
+ readonly sep: string;
46
+ };
47
+ /** Minimal `fs.Stats`-like surface for static file probes. */
48
+ export type StatResult = {
49
+ isFile(): boolean;
50
+ isDirectory(): boolean;
51
+ };
52
+ export type StatFn = (path: string) => Promise<StatResult>;
53
+ export type DistStaticResolveOpts = {
54
+ cookieHeader?: string;
55
+ };
56
+ /** Multipart form fields: text, File, or repeated same-name values. */
57
+ export type MultipartFieldValue = string | File | Array<string | File>;
58
+ export type MultipartFields = Record<string, MultipartFieldValue>;
59
+ export type OctetStreamBody = {
60
+ bytes: Buffer;
61
+ size: number;
62
+ key: string;
63
+ uploadId: string;
64
+ chunkIndex?: number;
65
+ chunkTotal?: number;
66
+ contentType: string;
67
+ };
@@ -3,7 +3,7 @@
3
3
  * Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
4
4
  */
5
5
  import { applyDirectHostBox, directHostBoxStyleAttr } from '../browser/direct-host-box.js';
6
- import { applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, findOwnedDefaultSlot, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from '../browser/dom-core.js';
6
+ import { applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, findOwnedDefaultSlot, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mergeClassParts, mergeStyleParts, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from '../browser/dom-core.js';
7
7
  import { markUnknownComponentHost, serializeUnknownComponentNode } from '../browser/unknown-component.js';
8
8
  let _ssrDocumentLastError = null;
9
9
  /**
@@ -549,7 +549,16 @@ const serializeApi = {
549
549
  return child;
550
550
  },
551
551
  el(tag) {
552
- return makeVirtualEl(tag);
552
+ const node = makeVirtualEl(tag);
553
+ if (String(tag || '').toLowerCase() === 'svg') {
554
+ node.namespaceURI = 'http://www.w3.org/2000/svg';
555
+ }
556
+ return node;
557
+ },
558
+ elNS(ns, tag) {
559
+ const node = makeVirtualEl(tag);
560
+ node.namespaceURI = ns || 'http://www.w3.org/2000/svg';
561
+ return node;
553
562
  },
554
563
  text(value) {
555
564
  const node = { __kind: 'text', value: value == null ? '' : String(value), parentNode: null };
@@ -633,6 +642,12 @@ const serializeApi = {
633
642
  return;
634
643
  applySerializeAttr(el, name, value);
635
644
  },
645
+ mergeClass(...parts) {
646
+ return mergeClassParts(...parts);
647
+ },
648
+ mergeStyle(...parts) {
649
+ return mergeStyleParts(...parts);
650
+ },
636
651
  on() {
637
652
  /* events are no-ops during SSR */
638
653
  },
@@ -666,6 +681,14 @@ const serializeApi = {
666
681
  catch {
667
682
  raw = null;
668
683
  }
684
+ if (name === 'class' || name === 'className') {
685
+ applySerializeAttr(el, 'class', mergeClassParts(raw));
686
+ return;
687
+ }
688
+ if (name === 'style') {
689
+ applySerializeAttr(el, 'style', mergeStyleParts(raw));
690
+ return;
691
+ }
669
692
  applySerializeAttr(el, name, raw);
670
693
  },
671
694
  bindComponentProp() {
@@ -677,7 +700,17 @@ const serializeApi = {
677
700
  // serializeApi.component returns a serialize el tree (or island shell).
678
701
  const root = hostEl.__kind === 'el' ? hostEl : null;
679
702
  const findOwnedSlot = (n) => {
680
- if (!n || n.__kind !== 'el')
703
+ if (!n || typeof n !== 'object')
704
+ return null;
705
+ if (n.__kind === 'frag') {
706
+ for (const c of n.children || []) {
707
+ const hit = findOwnedSlot(c);
708
+ if (hit)
709
+ return hit;
710
+ }
711
+ return null;
712
+ }
713
+ if (n.__kind !== 'el')
681
714
  return null;
682
715
  if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
683
716
  return n;
@@ -690,26 +723,52 @@ const serializeApi = {
690
723
  }
691
724
  return null;
692
725
  };
726
+ const findOwnedSlotTarget = (n) => {
727
+ const slot = findOwnedSlot(n);
728
+ if (slot)
729
+ return slot;
730
+ if (!n || n.__kind !== 'el')
731
+ return null;
732
+ const tag = String(n.tag || '').toLowerCase();
733
+ if ((tag === 'button' || tag === 'a') && n.attrs && n.attrs['data-vmz-ui'] === 'button')
734
+ return n;
735
+ if (n.attrs && n.attrs['data-vmz'] != null)
736
+ return null;
737
+ for (const c of n.children || []) {
738
+ if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
739
+ continue;
740
+ const hit = findOwnedSlotTarget(c);
741
+ if (hit)
742
+ return hit;
743
+ }
744
+ return null;
745
+ };
693
746
  // Prefer searching the component body (first child of host wrapper).
694
- let slot = null;
747
+ let target = null;
695
748
  if (root) {
696
749
  for (const c of root.children || []) {
697
750
  if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
698
751
  continue;
699
- slot = findOwnedSlot(c);
700
- if (slot)
752
+ target = findOwnedSlotTarget(c);
753
+ if (target)
701
754
  break;
702
755
  }
703
- if (!slot)
704
- slot = findOwnedSlot(root);
756
+ if (!target)
757
+ target = findOwnedSlotTarget(root);
705
758
  }
706
- if (slot) {
707
- slot.__rawHtml = null;
708
- if (!Array.isArray(slot.children))
709
- slot.children = [];
710
- // Append ??multiple projectDefaultSlot calls must accumulate (SSR).
711
- // Client path replaces the live <slot> then appends siblings; serialize must push.
712
- slot.children.push(node);
759
+ if (target) {
760
+ if (target.tag === 'slot') {
761
+ target.__rawHtml = null;
762
+ if (!Array.isArray(target.children))
763
+ target.children = [];
764
+ // Append ??multiple projectDefaultSlot calls must accumulate (SSR).
765
+ // Client path replaces the live <slot> then appends siblings; serialize must push.
766
+ target.children.push(node);
767
+ return;
768
+ }
769
+ if (!Array.isArray(target.children))
770
+ target.children = [];
771
+ target.children.push(node);
713
772
  return;
714
773
  }
715
774
  if (root)
@@ -833,6 +892,22 @@ function applySerializeAttr(el, name, value) {
833
892
  if (!el || el.__kind !== 'el')
834
893
  return;
835
894
  const key = name === 'className' ? 'class' : name;
895
+ if (key === 'class') {
896
+ const s = mergeClassParts(value);
897
+ if (s)
898
+ el.attrs[key] = s;
899
+ else
900
+ delete el.attrs[key];
901
+ return;
902
+ }
903
+ if (key === 'style') {
904
+ const s = mergeStyleParts(value);
905
+ if (s)
906
+ el.attrs[key] = s;
907
+ else
908
+ delete el.attrs[key];
909
+ return;
910
+ }
836
911
  if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
837
912
  if (value === false || value == null || value === '')
838
913
  delete el.attrs[key];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
6
6
  "exports": {
@@ -70,12 +70,12 @@
70
70
  "linkedom": "^0.18.13"
71
71
  },
72
72
  "optionalDependencies": {
73
- "@vmz/vmz-win32-x64": "0.2.0",
74
- "@vmz/vmz-win32-arm64": "0.2.0",
75
- "@vmz/vmz-darwin-x64": "0.2.0",
76
- "@vmz/vmz-darwin-arm64": "0.2.0",
77
- "@vmz/vmz-linux-x64": "0.2.0",
78
- "@vmz/vmz-linux-arm64": "0.2.0"
73
+ "@vmz/vmz-win32-x64": "0.2.2",
74
+ "@vmz/vmz-win32-arm64": "0.2.2",
75
+ "@vmz/vmz-darwin-x64": "0.2.2",
76
+ "@vmz/vmz-darwin-arm64": "0.2.2",
77
+ "@vmz/vmz-linux-x64": "0.2.2",
78
+ "@vmz/vmz-linux-arm64": "0.2.2"
79
79
  },
80
80
  "publishConfig": {
81
81
  "access": "public"