@microsoft/webui-framework 0.0.8 → 0.0.10

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.
@@ -1,3 +1,33 @@
1
+ /**
2
+ * Convert a camelCase DOM property name into its kebab-case HTML attribute form.
3
+ *
4
+ * This function is optimized for framework-level hot paths where attribute
5
+ * normalization may run thousands of times per render. It performs three
6
+ * progressively cheaper checks:
7
+ *
8
+ * 1. **Direct lookup for irregular mappings**
9
+ * Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
10
+ * follow simple camelCase → kebab-case rules. These are resolved through a
11
+ * precomputed `propertyToAttribute` map for O(1) returns with no string
12
+ * processing.
13
+ *
14
+ * 2. **Fast path for ARIA attributes**
15
+ * ARIA properties always begin with `aria` followed by an uppercase letter
16
+ * (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
17
+ * This branch avoids the general loop and uses the engine-optimized
18
+ * `.toLowerCase()` for the suffix.
19
+ *
20
+ * 3. **General camelCase → kebab-case conversion**
21
+ * For all other inputs, the function performs a tight ASCII-only scan:
22
+ * uppercase A–Z (65–90) are converted to lowercase and prefixed with `-`,
23
+ * while all other characters are copied as-is. This avoids regex engines,
24
+ * callback allocations, and match objects, producing predictable,
25
+ * allocation-minimal performance ideal for DOM attribute reflection.
26
+ *
27
+ * The result is a predictable, JIT-friendly transformation suitable for
28
+ * attribute diffing, SSR serialization, and runtime DOM patching.
29
+ */
30
+ export declare function toKebabCase(str: string): string;
1
31
  export declare function getObservableNames(ctor: Function): Set<string>;
2
32
  /**
3
33
  * Marks a property as observable. When the value changes the decorator will:
@@ -9,9 +9,80 @@
9
9
  // ---------------------------------------------------------------------------
10
10
  // Internal helpers
11
11
  // ---------------------------------------------------------------------------
12
- /** Convert camelCase to kebab-case for attribute reflection. */
13
- function toKebabCase(str) {
14
- return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
12
+ /**
13
+ * Map of camelCase property names to their HTML attribute names.
14
+ *
15
+ * ARIA attributes (`ariaXxxYyy → aria-` + lowercase remainder) are handled
16
+ * algorithmically in `toKebabCase`. Only HTML global/element attributes
17
+ * with irregular mappings (concatenated lowercase) need explicit entries.
18
+ */
19
+ const propertyToAttribute = Object.assign(Object.create(null), {
20
+ // --- HTML global/element attributes ---
21
+ accessKey: 'accesskey',
22
+ autoCapitalize: 'autocapitalize',
23
+ contentEditable: 'contenteditable',
24
+ crossOrigin: 'crossorigin',
25
+ dirName: 'dirname',
26
+ fetchPriority: 'fetchpriority',
27
+ formAction: 'formaction',
28
+ formEnctype: 'formenctype',
29
+ formMethod: 'formmethod',
30
+ formNoValidate: 'formnovalidate',
31
+ formTarget: 'formtarget',
32
+ inputMode: 'inputmode',
33
+ isMap: 'ismap',
34
+ maxLength: 'maxlength',
35
+ minLength: 'minlength',
36
+ noModule: 'nomodule',
37
+ noValidate: 'novalidate',
38
+ readOnly: 'readonly',
39
+ referrerPolicy: 'referrerpolicy',
40
+ tabIndex: 'tabindex',
41
+ useMap: 'usemap',
42
+ });
43
+ /**
44
+ * Convert a camelCase DOM property name into its kebab-case HTML attribute form.
45
+ *
46
+ * This function is optimized for framework-level hot paths where attribute
47
+ * normalization may run thousands of times per render. It performs three
48
+ * progressively cheaper checks:
49
+ *
50
+ * 1. **Direct lookup for irregular mappings**
51
+ * Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
52
+ * follow simple camelCase → kebab-case rules. These are resolved through a
53
+ * precomputed `propertyToAttribute` map for O(1) returns with no string
54
+ * processing.
55
+ *
56
+ * 2. **Fast path for ARIA attributes**
57
+ * ARIA properties always begin with `aria` followed by an uppercase letter
58
+ * (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
59
+ * This branch avoids the general loop and uses the engine-optimized
60
+ * `.toLowerCase()` for the suffix.
61
+ *
62
+ * 3. **General camelCase → kebab-case conversion**
63
+ * For all other inputs, the function performs a tight ASCII-only scan:
64
+ * uppercase A–Z (65–90) are converted to lowercase and prefixed with `-`,
65
+ * while all other characters are copied as-is. This avoids regex engines,
66
+ * callback allocations, and match objects, producing predictable,
67
+ * allocation-minimal performance ideal for DOM attribute reflection.
68
+ *
69
+ * The result is a predictable, JIT-friendly transformation suitable for
70
+ * attribute diffing, SSR serialization, and runtime DOM patching.
71
+ */
72
+ export function toKebabCase(str) {
73
+ const mapped = propertyToAttribute[str];
74
+ if (mapped)
75
+ return mapped;
76
+ // ARIA properties: ariaXxxYyy → aria- + lowercase remainder
77
+ if (str.length > 4 && str.charCodeAt(0) === 97 /* a */ && str.startsWith('aria') && str.charCodeAt(4) >= 65 && str.charCodeAt(4) <= 90) {
78
+ return 'aria-' + str.slice(4).toLowerCase();
79
+ }
80
+ let out = '';
81
+ for (let i = 0; i < str.length; i++) {
82
+ const code = str.charCodeAt(i);
83
+ out += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];
84
+ }
85
+ return out;
15
86
  }
16
87
  /**
17
88
  * Shared logic for installing a reactive getter/setter on a class prototype.
@@ -28,7 +28,7 @@ export function dotWalk(cursor, path, from) {
28
28
  export function resolveRepeatValue(scopeVar, scope, path) {
29
29
  if (path === scopeVar)
30
30
  return scope;
31
- if (!path.startsWith(`${scopeVar}.`))
31
+ if (path.length <= scopeVar.length || path.charCodeAt(scopeVar.length) !== 46 /* '.' */ || !path.startsWith(scopeVar))
32
32
  return undefined;
33
33
  return dotWalk(scope, path, scopeVar.length + 1);
34
34
  }
package/dist/element.js CHANGED
@@ -48,8 +48,8 @@ import { ATTR_KIND_BOOLEAN, ATTR_KIND_COMPLEX, ATTR_KIND_TEMPLATE, } from './ele
48
48
  // ── Caches ──────────────────────────────────────────────────────
49
49
  /** Parsed template cache — cloneNode(true) is faster than re-parsing. */
50
50
  const templateCache = new WeakMap();
51
- /** Parsed template DOM for SSR path mapping, keyed by meta.h string. */
52
- const templateDOMCache = new Map();
51
+ /** Parsed template DOM for SSR path mapping, keyed by TemplateBlockMeta. */
52
+ const templateDOMCache = new WeakMap();
53
53
  /** Pre-computed ordinals for template nodes: childIndex → [nodeType, ordinal].
54
54
  * Avoids re-counting element/text siblings on every $resolveSSR call. */
55
55
  const tplOrdinalCache = new WeakMap();
@@ -88,12 +88,12 @@ function childNodesArray(parent) {
88
88
  }
89
89
  // ── Helper: parse template HTML into a temp container ────────────
90
90
  function getTemplateDom(meta) {
91
- let cached = templateDOMCache.get(meta.h);
91
+ let cached = templateDOMCache.get(meta);
92
92
  if (cached)
93
93
  return cached;
94
94
  const div = document.createElement('div');
95
95
  div.innerHTML = meta.h;
96
- templateDOMCache.set(meta.h, div);
96
+ templateDOMCache.set(meta, div);
97
97
  return div;
98
98
  }
99
99
  // ═══════════════════════════════════════════════════════════════════
@@ -208,7 +208,12 @@ export class WebUIElement extends HTMLElement {
208
208
  }
209
209
  hydrationEnd();
210
210
  }
211
- disconnectedCallback() { }
211
+ disconnectedCallback() {
212
+ // Note: event listeners wired by $addEvent target child nodes owned by
213
+ // this component — they will be GC'd together with the component.
214
+ // We intentionally do NOT remove them here because connectedCallback
215
+ // does not re-wire events when a hydrated component is reattached.
216
+ }
212
217
  /** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
213
218
  $emit(name, detail) {
214
219
  return this.dispatchEvent(new CustomEvent(name, {
@@ -775,23 +780,25 @@ export class WebUIElement extends HTMLElement {
775
780
  */
776
781
  $hydrateCondContent(condAnchor, blockMeta, scope) {
777
782
  const rootTag = this.$rootTag(blockMeta);
778
- if (rootTag) {
783
+ const tplDom = getTemplateDom(blockMeta);
784
+ if (rootTag && tplDom.children.length === 1) {
785
+ // Single-root optimisation: hydrate the element in-place (pathStart=1).
779
786
  const el = nextElement(condAnchor);
780
787
  if (el) {
781
- const inst = this.$hydrate(el, blockMeta, getTemplateDom(blockMeta), scope, 1);
788
+ const inst = this.$hydrate(el, blockMeta, tplDom, scope, 1);
782
789
  this.$updateInstance(inst);
783
790
  return inst;
784
791
  }
785
792
  return null;
786
793
  }
787
- // Text-only conditional: collect nodes between <!--wc--> and <!--/wc-->
794
+ // Multi-root or text-only conditional: collect nodes between <!--wc--> and <!--/wc-->
788
795
  const condNodes = this.$collectBetween(condAnchor, MARKER_COND_END);
789
796
  if (condNodes.length === 0)
790
797
  return null;
791
798
  const wrapper = document.createElement('div');
792
799
  for (let cn = 0; cn < condNodes.length; cn++)
793
800
  wrapper.appendChild(condNodes[cn]);
794
- const inst = this.$hydrate(wrapper, blockMeta, getTemplateDom(blockMeta), scope);
801
+ const inst = this.$hydrate(wrapper, blockMeta, tplDom, scope);
795
802
  inst.nodes = childNodesArray(wrapper);
796
803
  let afterNode = condAnchor;
797
804
  for (let cn = 0; cn < inst.nodes.length; cn++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/webui-framework",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "description": "WebUI Framework Next — Preact-inspired lightweight Web Component runtime with SSR hydration. 15KB minified, compiled-template path mapping, no hydration markers.",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "@playwright/test": "^1.58.2",
20
20
  "@types/node": "^25.3.5",
21
21
  "typescript": "^5.9.3",
22
- "@microsoft/webui-test-support": "0.0.8"
22
+ "@microsoft/webui-test-support": "0.0.10"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc",