@vmz/core 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,12 @@ export declare function findOwnedDefaultSlot(root: any): any;
5
5
  export declare function findOwnedDefaultSlotTarget(root: any): any;
6
6
  export declare function createInstance(Component: any, props?: any): any;
7
7
  export declare function destroy(inst: any): void;
8
+ /**
9
+ * DocumentFragment anchors are emptied by insertion (children move out, the
10
+ * fragment keeps no parentNode), so a projected fragment must anchor on its
11
+ * last child. Capture before inserting.
12
+ */
13
+ export declare function slotAnchorFor(node: any): any;
8
14
  export declare function insertProjectedSlotNode(target: any, node: any): any;
9
15
  /**
10
16
  * Hold projected children until the component's default slot exists.
@@ -183,15 +183,24 @@ export function destroy(inst) {
183
183
  }
184
184
  }
185
185
  }
186
+ /**
187
+ * DocumentFragment anchors are emptied by insertion (children move out, the
188
+ * fragment keeps no parentNode), so a projected fragment must anchor on its
189
+ * last child. Capture before inserting.
190
+ */
191
+ export function slotAnchorFor(node) {
192
+ return node && node.nodeType === 11 ? node.lastChild : node;
193
+ }
186
194
  export function insertProjectedSlotNode(target, node) {
187
195
  if (!target || !node)
188
196
  return null;
197
+ const anchor = slotAnchorFor(node);
189
198
  if (String(target.tagName || '').toLowerCase() === 'slot') {
190
199
  target.replaceWith(node);
191
- return node;
200
+ return anchor;
192
201
  }
193
202
  target.appendChild(node);
194
- return node;
203
+ return anchor;
195
204
  }
196
205
  /**
197
206
  * Hold projected children until the component's default slot exists.
@@ -245,23 +254,24 @@ export function flushParkedSlotNodes(hostEl) {
245
254
  hostEl.__vmzSlotObserver = null;
246
255
  }
247
256
  hostEl.__vmzSlotParked = [];
257
+ const anchor = slotAnchorFor(parked[parked.length - 1]);
248
258
  const tag = String(target.tagName || '').toLowerCase();
249
259
  if (tag === 'slot') {
250
260
  const frag = typeof document !== 'undefined' ? document.createDocumentFragment() : null;
251
261
  if (!frag) {
252
262
  target.replaceWith(parked[0]);
253
- hostEl.__vmzSlotAnchor = parked[parked.length - 1];
263
+ hostEl.__vmzSlotAnchor = anchor;
254
264
  return true;
255
265
  }
256
266
  for (const node of parked)
257
267
  frag.appendChild(node);
258
268
  target.replaceWith(frag);
259
- hostEl.__vmzSlotAnchor = parked[parked.length - 1];
269
+ hostEl.__vmzSlotAnchor = anchor;
260
270
  return true;
261
271
  }
262
272
  for (const node of parked)
263
273
  target.appendChild(node);
264
- hostEl.__vmzSlotAnchor = parked[parked.length - 1];
274
+ hostEl.__vmzSlotAnchor = anchor;
265
275
  return true;
266
276
  }
267
277
  export function disposeDomTree(root) {
@@ -13,6 +13,8 @@ export declare const directApi: {
13
13
  el(tag: any): any;
14
14
  elNS(ns: any, tag: any): Element;
15
15
  text(value: any): Text;
16
+ nsScopeDepth(): number;
17
+ rewindNsScope(depth: any): void;
16
18
  adoptEnter(node: any): boolean;
17
19
  adoptLeave(): void;
18
20
  frag(): DocumentFragment;
@@ -2,11 +2,34 @@ import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normaliz
2
2
  import { applyDirectHostBox } from '../direct-host-box.js';
3
3
  import { createUnknownComponentElement } from '../unknown-component.js';
4
4
  import { applyDomAttr, eventPropHandlerName } from './attributes.js';
5
- import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, insertProjectedSlotNode, parkSlotNode, runDirectCreate, stripFns, } from './component.js';
5
+ import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
6
6
  import { isEventEntryStrategy, noteDomCreate, noteDomRemove, scheduleClientOn } from './lifecycle.js';
7
7
  import { components, resolveComponent } from './registry.js';
8
8
  import { beginEventFlush, endEventFlush, inferHandlerMethod, methodAllowsSyncEventFlush, registerBind, runDomEventHandler, runPatch, scheduleRefresh, unregisterBind, } from '../reactivity/patch-scheduler.js';
9
9
  let directPropBindSeq = 0;
10
+ const SVG_NS = 'http://www.w3.org/2000/svg';
11
+ /**
12
+ * Nesting depth of inline `<svg>` subtrees during create/resume.
13
+ *
14
+ * Template emit creates every element through `api.el(tag)` and brackets each
15
+ * subtree with `adoptEnter`/`adoptLeave`. Elements inside `<svg>` must be built
16
+ * with `createElementNS`, otherwise the browser returns an HTMLUnknownElement:
17
+ * attribute names lose their case (`viewBox` → `viewbox`) and the node lays out
18
+ * at 0×0. Track the depth here so the generator stays namespace-agnostic.
19
+ */
20
+ const nsStack = [];
21
+ function inSvgScope() {
22
+ return nsStack.length > 0 && nsStack[nsStack.length - 1] === true;
23
+ }
24
+ function isSvgNode(node) {
25
+ if (!node || typeof node !== 'object')
26
+ return false;
27
+ const ns = node.namespaceURI;
28
+ if (ns === SVG_NS)
29
+ return true;
30
+ const tag = node.tagName;
31
+ return tag != null && String(tag).toLowerCase() === 'svg' && ns == null;
32
+ }
10
33
  export const directApi = {
11
34
  _inst: null,
12
35
  _branchBinds: null,
@@ -14,17 +37,19 @@ export const directApi = {
14
37
  _eachCtx: null,
15
38
  _resumeAdopt: null,
16
39
  el(tag) {
40
+ const name = tag || 'div';
41
+ const svg = inSvgScope() || name === 'svg';
17
42
  if (directApi._resumeAdopt)
18
- return directApi._resumeAdopt.el(tag);
43
+ return directApi._resumeAdopt.el(name, svg);
19
44
  noteDomCreate();
20
- return document.createElement(tag || 'div');
45
+ return svg ? document.createElementNS(SVG_NS, name) : document.createElement(name);
21
46
  },
22
47
  elNS(ns, tag) {
23
48
  if (directApi._resumeAdopt && typeof directApi._resumeAdopt.elNS === 'function') {
24
49
  return directApi._resumeAdopt.elNS(ns, tag);
25
50
  }
26
51
  noteDomCreate();
27
- return document.createElementNS(ns || 'http://www.w3.org/2000/svg', tag || 'g');
52
+ return document.createElementNS(ns || SVG_NS, tag || 'g');
28
53
  },
29
54
  text(value) {
30
55
  if (directApi._resumeAdopt)
@@ -32,7 +57,16 @@ export const directApi = {
32
57
  noteDomCreate();
33
58
  return document.createTextNode(value == null ? '' : String(value));
34
59
  },
60
+ nsScopeDepth() {
61
+ return nsStack.length;
62
+ },
63
+ rewindNsScope(depth) {
64
+ const target = Math.max(0, Number(depth) || 0);
65
+ while (nsStack.length > target)
66
+ nsStack.pop();
67
+ },
35
68
  adoptEnter(node) {
69
+ nsStack.push(isSvgNode(node));
36
70
  const adopt = directApi._resumeAdopt;
37
71
  if (adopt && typeof adopt.enter === 'function') {
38
72
  const ok = adopt.enter(node);
@@ -43,6 +77,7 @@ export const directApi = {
43
77
  return false;
44
78
  },
45
79
  adoptLeave() {
80
+ nsStack.pop();
46
81
  const adopt = directApi._resumeAdopt;
47
82
  // Emit always pairs leave after enter; if enter failed (fresh node, no
48
83
  // pool), do not pop ??unbalanced leave previously stole parent scopes
@@ -438,8 +473,9 @@ export const directApi = {
438
473
  // call appends after the last one instead of re-searching for a slot.
439
474
  const anchor = hostEl.__vmzSlotAnchor;
440
475
  if (anchor && anchor.parentNode) {
476
+ const nextAnchor = slotAnchorFor(node);
441
477
  anchor.parentNode.insertBefore(node, anchor.nextSibling);
442
- hostEl.__vmzSlotAnchor = node;
478
+ hostEl.__vmzSlotAnchor = nextAnchor;
443
479
  return;
444
480
  }
445
481
  // Component slot may live inside an unmounted `v-if` branch (closed overlay,
@@ -69,7 +69,7 @@ export type TraceBuffer = {
69
69
  events: TraceEvent[];
70
70
  };
71
71
  export type ResumeAdoptCtx = {
72
- el: (tag: string) => Element;
72
+ el: (tag: string, svg?: boolean) => Element;
73
73
  elNS?: (ns: string, tag: string) => Element;
74
74
  text: (value?: unknown) => Text;
75
75
  componentHost?: (name: string) => HTMLElement | null;
@@ -1,19 +1,8 @@
1
1
  import { directApi, noteDomCreate, runDirectCreate, } from '../dom-core.js';
2
+ const SVG_NS = 'http://www.w3.org/2000/svg';
2
3
  function createResumeAdopt(pool, rootEl) {
3
4
  const used = new WeakSet();
4
5
  const scopeStack = [pool];
5
- const walkTexts = (root, visit) => {
6
- if (!root)
7
- return;
8
- if (root.nodeType === 3) {
9
- visit(root);
10
- return;
11
- }
12
- if (root.nodeType !== 1)
13
- return;
14
- for (let c = root.firstChild; c; c = c.nextSibling)
15
- walkTexts(c, visit);
16
- };
17
6
  const currentScope = () => scopeStack[scopeStack.length - 1] || pool;
18
7
  const markElement = (n) => {
19
8
  used.add(n);
@@ -42,27 +31,36 @@ function createResumeAdopt(pool, rootEl) {
42
31
  }
43
32
  }
44
33
  }
34
+ /**
35
+ * Text bindings always belong to the element whose scope is active, so only
36
+ * direct text children are candidates. Recursing into element children
37
+ * lets an earlier binding steal the text of a sibling that has not been
38
+ * adopted yet (soft navigation lost the landing CTA label this way).
39
+ */
45
40
  function* textCandidates() {
46
41
  const scope = currentScope();
47
- const kids = [...scope.childNodes];
48
- for (const n of kids) {
49
- const found = [];
50
- walkTexts(n, (t) => {
51
- if (!used.has(t))
52
- found.push(t);
53
- });
54
- for (const t of found)
55
- yield t;
42
+ for (const n of [...scope.childNodes]) {
43
+ if (n.nodeType === 3 && !used.has(n))
44
+ yield n;
56
45
  }
57
46
  }
58
- const adoptElement = (tag) => {
47
+ const adoptElement = (tag, svg = false) => {
59
48
  const want = String(tag || 'div').toLowerCase();
60
49
  for (const el of elementCandidates()) {
61
50
  if (String(el.tagName).toLowerCase() === want)
62
51
  return markElement(el);
63
52
  }
64
53
  noteDomCreate();
65
- return document.createElement(tag || 'div');
54
+ return svg ? document.createElementNS(SVG_NS, tag || 'svg') : document.createElement(tag || 'div');
55
+ };
56
+ const adoptElementNS = (ns, tag) => {
57
+ const want = String(tag || 'g');
58
+ for (const el of elementCandidates()) {
59
+ if (el.namespaceURI === ns && String(el.localName) === want)
60
+ return markElement(el);
61
+ }
62
+ noteDomCreate();
63
+ return document.createElementNS(ns || SVG_NS, want);
66
64
  };
67
65
  const adoptText = (value) => {
68
66
  for (const n of textCandidates()) {
@@ -125,6 +123,7 @@ function createResumeAdopt(pool, rootEl) {
125
123
  };
126
124
  return {
127
125
  el: adoptElement,
126
+ elNS: adoptElementNS,
128
127
  text: adoptText,
129
128
  componentHost,
130
129
  enter,
@@ -148,6 +147,7 @@ export function runDirectResume(Component, inst, container) {
148
147
  const prevItems = directApi._itemPatches;
149
148
  const prevEach = directApi._eachCtx;
150
149
  const prevAdopt = directApi._resumeAdopt;
150
+ const prevNsDepth = directApi.nsScopeDepth();
151
151
  directApi._inst = inst;
152
152
  directApi._branchBinds = null;
153
153
  directApi._itemPatches = null;
@@ -157,6 +157,7 @@ export function runDirectResume(Component, inst, container) {
157
157
  return Component.__vmzCreate.call(inst, directApi);
158
158
  }
159
159
  finally {
160
+ directApi.rewindNsScope(prevNsDepth);
160
161
  directApi._resumeAdopt = prevAdopt;
161
162
  directApi._inst = prevInst;
162
163
  directApi._branchBinds = prevBranch;
@@ -222,11 +222,11 @@ async function* emitAccessShell(marker) {
222
222
  bodyAttrs: [],
223
223
  });
224
224
  }
225
- function resolvePageDocumentMeta(Page) {
225
+ function resolvePageDocumentMeta(Page, ctx = {}) {
226
226
  try {
227
227
  let raw = {};
228
228
  if (typeof Page?.meta === 'function')
229
- raw = Page.meta() || {};
229
+ raw = Page.meta(ctx) || {};
230
230
  else if (Page?.meta && typeof Page.meta === 'object')
231
231
  raw = Page.meta;
232
232
  const title = String(raw.title || '').trim();
@@ -349,7 +349,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
349
349
  htmlExtraAttrs.push('data-vmz-locale-hrefs', JSON.stringify(hrefTable));
350
350
  }
351
351
  }
352
- const pageDocMeta = resolvePageDocumentMeta(Page);
352
+ const pageDocMeta = resolvePageDocumentMeta(Page, { localeId });
353
353
  const prevLocaleHint = globalThis.__vmzLocaleIdHint;
354
354
  globalThis.__vmzLocaleIdHint = localeId;
355
355
  if (!serveState.ssrRenderHost) {
@@ -58,8 +58,12 @@ export function ensureSsrDocument() {
58
58
  const detail = errors.length ? `\n${errors.join('\n')}` : '';
59
59
  throw new Error(`linkedom unresolved for SSR document${detail}`);
60
60
  }
61
- const { window, document } = parseHTML('<!DOCTYPE html><html><body></body></html>');
62
- globalThis.window = window;
61
+ // SSR host contract: expose `document` only. Installing the linkedom
62
+ // `window` would make `typeof window !== "undefined"` true on the server
63
+ // while still having no browser `location`, so client code would take
64
+ // browser branches and diverge from the browser lifecycle. The window is
65
+ // still reachable as `document.defaultView` when DOM APIs need it.
66
+ const { document } = parseHTML('<!DOCTYPE html><html><body></body></html>');
63
67
  globalThis.document = document;
64
68
  _ssrDocumentLastError = null;
65
69
  return typeof document.createElement === 'function';
package/package.json CHANGED
@@ -68,12 +68,12 @@
68
68
  "main": "./dist/faces/server.js",
69
69
  "name": "@vmz/core",
70
70
  "optionalDependencies": {
71
- "@vmz/vmz-darwin-arm64": "0.3.7",
72
- "@vmz/vmz-darwin-x64": "0.3.7",
73
- "@vmz/vmz-linux-arm64": "0.3.7",
74
- "@vmz/vmz-linux-x64": "0.3.7",
75
- "@vmz/vmz-win32-arm64": "0.3.7",
76
- "@vmz/vmz-win32-x64": "0.3.7"
71
+ "@vmz/vmz-darwin-arm64": "0.3.9",
72
+ "@vmz/vmz-darwin-x64": "0.3.9",
73
+ "@vmz/vmz-linux-arm64": "0.3.9",
74
+ "@vmz/vmz-linux-x64": "0.3.9",
75
+ "@vmz/vmz-win32-arm64": "0.3.9",
76
+ "@vmz/vmz-win32-x64": "0.3.9"
77
77
  },
78
78
  "publishConfig": {
79
79
  "access": "public"
@@ -88,5 +88,5 @@
88
88
  },
89
89
  "type": "module",
90
90
  "types": "./dist/faces/index.d.ts",
91
- "version": "0.3.7"
91
+ "version": "0.3.9"
92
92
  }