@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.
- package/dist/browser/direct-api.types.d.ts +94 -11
- package/dist/browser/dom-core.d.ts +6 -0
- package/dist/browser/dom-core.js +74 -8
- package/dist/browser/dom-core.types.d.ts +4 -14
- package/dist/browser/unknown-component.js +1 -2
- package/dist/faces/dom.browser.d.ts +1 -1
- package/dist/faces/dom.browser.js +1 -1
- package/dist/faces/http.d.ts +6 -6
- package/dist/faces/http.js +1 -1
- package/dist/faces/server.d.ts +8 -7
- package/dist/faces/server.js +44 -36
- package/dist/host/render-host.d.ts +2 -2
- package/dist/host/render-host.js +1 -1
- package/dist/shared/dom-attr-normalize.d.ts +8 -0
- package/dist/shared/dom-attr-normalize.js +71 -0
- package/dist/shared/host.types.d.ts +47 -1
- package/dist/shared/server.types.d.ts +28 -0
- package/dist/ssr/dom-ssr.js +90 -15
- package/package.json +7 -7
|
@@ -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:
|
|
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:
|
|
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).
|
package/dist/browser/dom-core.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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,
|
|
5
|
-
export type { DirectApi, DirectInstance, PatchFn,
|
|
6
|
-
export type ComponentCtor =
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
+
export { attachEventEntries, hydrate, hydrateIslands, hydrateRoute, hydrateRoutePage, resume, resumeIslands } from '../ssr/dom-ssr.js';
|
package/dist/faces/http.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** Compile-time HTTP decorators
|
|
2
|
-
export declare function Get(path:
|
|
3
|
-
export declare function Post(path:
|
|
4
|
-
export declare function Put(path:
|
|
5
|
-
export declare function Delete(path:
|
|
6
|
-
export declare function Patch(path:
|
|
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;
|
package/dist/faces/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Compile-time HTTP decorators
|
|
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;
|
package/dist/faces/server.d.ts
CHANGED
|
@@ -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 {
|
|
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`
|
|
10
|
-
* Only set this in Node/SSR hosts
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
30
|
+
export declare function handleNodeRequest(req: IncomingMessage, res: ServerResponse, opts?: NodeRequestOptions): Promise<void>;
|
package/dist/faces/server.js
CHANGED
|
@@ -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`
|
|
12
|
-
* Only set this in Node/SSR hosts
|
|
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
|
|
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
|
|
35
|
-
if (typeof
|
|
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
|
|
88
|
-
* Multipart
|
|
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
|
|
110
|
-
// (0xFF/0xFE
|
|
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
|
|
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
|
|
183
|
-
// static route HTML (`index.html`, `about/index.html`,
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
|
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/`
|
|
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) =>
|
|
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) =>
|
|
471
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
@@ -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
|
|
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
|
|
656
|
-
// (not headersSent + destroy
|
|
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
|
|
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
|
|
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
|
|
11
|
-
closureChunkIds: (rootChunkIds
|
|
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;
|
package/dist/host/render-host.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/ssr/dom-ssr.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
700
|
-
if (
|
|
752
|
+
target = findOwnedSlotTarget(c);
|
|
753
|
+
if (target)
|
|
701
754
|
break;
|
|
702
755
|
}
|
|
703
|
-
if (!
|
|
704
|
-
|
|
756
|
+
if (!target)
|
|
757
|
+
target = findOwnedSlotTarget(root);
|
|
705
758
|
}
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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.
|
|
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.
|
|
74
|
-
"@vmz/vmz-win32-arm64": "0.2.
|
|
75
|
-
"@vmz/vmz-darwin-x64": "0.2.
|
|
76
|
-
"@vmz/vmz-darwin-arm64": "0.2.
|
|
77
|
-
"@vmz/vmz-linux-x64": "0.2.
|
|
78
|
-
"@vmz/vmz-linux-arm64": "0.2.
|
|
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"
|