@esportsplus/template 0.16.14 → 0.16.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bench/runtime.bench.ts +207 -0
  2. package/build/attributes.js +4 -1
  3. package/build/slot/array.js +50 -4
  4. package/build/slot/render.js +1 -2
  5. package/build/utilities.d.ts +2 -1
  6. package/build/utilities.js +2 -1
  7. package/package.json +2 -1
  8. package/src/attributes.ts +4 -1
  9. package/src/slot/array.ts +76 -4
  10. package/src/slot/render.ts +1 -4
  11. package/src/utilities.ts +3 -1
  12. package/storage/feature-research-2026-03-24.md +475 -0
  13. package/test-output.txt +0 -0
  14. package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
  15. package/tests/compiler/codegen.ts +292 -0
  16. package/tests/compiler/integration.ts +252 -0
  17. package/tests/compiler/ts-parser.ts +160 -0
  18. package/{test/constants.test.ts → tests/constants.ts} +5 -1
  19. package/tests/event/onconnect.ts +147 -0
  20. package/tests/event/onresize.ts +187 -0
  21. package/tests/event/ontick.ts +273 -0
  22. package/{test/slot/array.test.ts → tests/slot/array.ts} +274 -0
  23. package/vitest.bench.config.ts +18 -0
  24. package/vitest.config.ts +1 -1
  25. package/storage/compiler-architecture-2026-01-13.md +0 -420
  26. /package/{test → examples}/index.ts +0 -0
  27. /package/{test → examples}/vite.config.ts +0 -0
  28. /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
  29. /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
  30. /package/{test → tests}/dist/test.js +0 -0
  31. /package/{test → tests}/dist/test.js.map +0 -0
  32. /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
  33. /package/{test/html.test.ts → tests/html.ts} +0 -0
  34. /package/{test/render.test.ts → tests/render.ts} +0 -0
  35. /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
  36. /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
  37. /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
  38. /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
  39. /package/{test/svg.test.ts → tests/svg.ts} +0 -0
  40. /package/{test/utilities.test.ts → tests/utilities.ts} +0 -0
@@ -0,0 +1,207 @@
1
+ import { bench, describe } from 'vitest';
2
+
3
+
4
+ describe('attributes - apply', () => {
5
+ let element: HTMLDivElement;
6
+
7
+ bench('setAttribute style', () => {
8
+ element = document.createElement('div');
9
+ element.setAttribute('style', 'color: red; font-size: 14px; display: flex;');
10
+ });
11
+
12
+ bench('style.cssText', () => {
13
+ element = document.createElement('div');
14
+ element.style.cssText = 'color: red; font-size: 14px; display: flex;';
15
+ });
16
+
17
+ bench('className assignment', () => {
18
+ element = document.createElement('div');
19
+ element.className = 'foo bar baz qux';
20
+ });
21
+
22
+ bench('setAttribute class', () => {
23
+ element = document.createElement('div');
24
+ element.setAttribute('class', 'foo bar baz qux');
25
+ });
26
+ });
27
+
28
+
29
+ describe('attributes - class list rebuild', () => {
30
+ bench('Set for..of + string concat', () => {
31
+ let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']),
32
+ result = '';
33
+
34
+ for (let key of set) {
35
+ result += (result ? ' ' : '') + key;
36
+ }
37
+ });
38
+
39
+ bench('Array.from(set).join', () => {
40
+ let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']);
41
+
42
+ Array.from(set).join(' ');
43
+ });
44
+
45
+ bench('set forEach + string concat', () => {
46
+ let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']),
47
+ result = '';
48
+
49
+ set.forEach(key => {
50
+ result += (result ? ' ' : '') + key;
51
+ });
52
+ });
53
+ });
54
+
55
+
56
+ describe('event - defineProperty overhead', () => {
57
+ let event: Event;
58
+
59
+ bench('defineProperty per dispatch', () => {
60
+ event = new Event('click');
61
+ let node: HTMLElement | null = document.createElement('div');
62
+
63
+ Object.defineProperty(event, 'currentTarget', {
64
+ configurable: true,
65
+ get() {
66
+ return node || document;
67
+ }
68
+ });
69
+ });
70
+
71
+ bench('defineProperty once + closure update', () => {
72
+ event = new Event('click');
73
+ let currentNode: HTMLElement | null = null;
74
+
75
+ Object.defineProperty(event, 'currentTarget', {
76
+ configurable: true,
77
+ get() {
78
+ return currentNode || document;
79
+ }
80
+ });
81
+
82
+ currentNode = document.createElement('div');
83
+ });
84
+ });
85
+
86
+
87
+ describe('marker - comment vs text node', () => {
88
+ let comment = document.createComment('$'),
89
+ textNode = document.createTextNode('');
90
+
91
+ bench('clone comment node', () => {
92
+ comment.cloneNode();
93
+ });
94
+
95
+ bench('clone text node', () => {
96
+ textNode.cloneNode();
97
+ });
98
+ });
99
+
100
+
101
+ describe('ontick - Set iteration', () => {
102
+ let tasks = new Set<VoidFunction>();
103
+
104
+ for (let i = 0; i < 10; i++) {
105
+ tasks.add(() => {});
106
+ }
107
+
108
+ bench('for..of Set', () => {
109
+ for (let task of tasks) {
110
+ task();
111
+ }
112
+ });
113
+
114
+ bench('Set.forEach', () => {
115
+ tasks.forEach(task => task());
116
+ });
117
+ });
118
+
119
+
120
+ describe('array sync - fragment append', () => {
121
+ let fragment: DocumentFragment,
122
+ nodes: Node[];
123
+
124
+ bench('individual append', () => {
125
+ fragment = document.createDocumentFragment();
126
+ nodes = [];
127
+
128
+ for (let i = 0; i < 50; i++) {
129
+ nodes.push(document.createElement('div'));
130
+ }
131
+
132
+ for (let i = 0, n = nodes.length; i < n; i++) {
133
+ fragment.append(nodes[i]);
134
+ }
135
+ });
136
+
137
+ bench('batch append spread', () => {
138
+ fragment = document.createDocumentFragment();
139
+ nodes = [];
140
+
141
+ for (let i = 0; i < 50; i++) {
142
+ nodes.push(document.createElement('div'));
143
+ }
144
+
145
+ fragment.append(...nodes);
146
+ });
147
+ });
148
+
149
+
150
+ describe('array sort - full resync vs minimal moves', () => {
151
+ let parent: HTMLDivElement,
152
+ fragment: DocumentFragment;
153
+
154
+ bench('full detach + reattach (current)', () => {
155
+ parent = document.createElement('div');
156
+ fragment = document.createDocumentFragment();
157
+
158
+ for (let i = 0; i < 50; i++) {
159
+ parent.appendChild(document.createElement('span'));
160
+ }
161
+
162
+ let children = Array.from(parent.children);
163
+
164
+ // Simulate: detach all, reattach in new order
165
+ for (let i = 0, n = children.length; i < n; i++) {
166
+ fragment.append(children[i]);
167
+ }
168
+
169
+ parent.appendChild(fragment);
170
+ });
171
+
172
+ bench('targeted insertBefore (LIS approach)', () => {
173
+ parent = document.createElement('div');
174
+
175
+ for (let i = 0; i < 50; i++) {
176
+ parent.appendChild(document.createElement('span'));
177
+ }
178
+
179
+ let children = Array.from(parent.children);
180
+
181
+ // Simulate: only move 5 out of 50 elements (90% stay)
182
+ for (let i = 0; i < 5; i++) {
183
+ let idx = Math.floor(Math.random() * children.length);
184
+
185
+ parent.insertBefore(children[idx], children[(idx + 10) % children.length]);
186
+ }
187
+ });
188
+ });
189
+
190
+
191
+ describe('fragment - dedup empty', () => {
192
+ let tmpl = document.createElement('template');
193
+
194
+ bench('fragment() call', () => {
195
+ let element = tmpl.cloneNode() as HTMLTemplateElement;
196
+
197
+ element.innerHTML = '';
198
+
199
+ element.content;
200
+ });
201
+
202
+ bench('cached fragment clone', () => {
203
+ let cached = document.createDocumentFragment();
204
+
205
+ cached.cloneNode(true);
206
+ });
207
+ });
@@ -12,7 +12,10 @@ function apply(element, name, value) {
12
12
  else if (name === 'class') {
13
13
  element.className = value;
14
14
  }
15
- else if (name === 'style' || (name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
15
+ else if (name === 'style') {
16
+ element.style.cssText = value;
17
+ }
18
+ else if ((name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
16
19
  element.setAttribute(name, value);
17
20
  }
18
21
  else {
@@ -1,8 +1,37 @@
1
1
  import { read, root, signal, write } from '@esportsplus/reactivity';
2
2
  import { ARRAY_SLOT } from '../constants.js';
3
- import { clone, fragment, marker, raf } from '../utilities.js';
3
+ import { clone, EMPTY_FRAGMENT, marker, raf } from '../utilities.js';
4
4
  import { ondisconnect, remove } from './cleanup.js';
5
- const EMPTY_FRAGMENT = fragment('');
5
+ function lis(arr) {
6
+ let n = arr.length;
7
+ if (n === 0) {
8
+ return new Set();
9
+ }
10
+ let ends = new Int32Array(n), predecessors = new Int32Array(n), len = 0;
11
+ for (let i = 0; i < n; i++) {
12
+ let lo = 0, hi = len, val = arr[i];
13
+ while (lo < hi) {
14
+ let mid = (lo + hi) >> 1;
15
+ if (arr[ends[mid]] < val) {
16
+ lo = mid + 1;
17
+ }
18
+ else {
19
+ hi = mid;
20
+ }
21
+ }
22
+ ends[lo] = i;
23
+ predecessors[i] = lo > 0 ? ends[lo - 1] : -1;
24
+ if (lo >= len) {
25
+ len = lo + 1;
26
+ }
27
+ }
28
+ let idx = ends[len - 1], result = new Set();
29
+ for (let i = len - 1; i >= 0; i--) {
30
+ result.add(idx);
31
+ idx = predecessors[idx];
32
+ }
33
+ return result;
34
+ }
6
35
  class ArraySlot {
7
36
  array;
8
37
  marker;
@@ -153,12 +182,29 @@ class ArraySlot {
153
182
  this.marker.after(this.fragment);
154
183
  return;
155
184
  }
156
- let sorted = new Array(n);
185
+ let end = n > 0 ? nodes[n - 1].tail.nextSibling : null, keep = lis(order), parent = this.marker.parentNode, sorted = new Array(n);
157
186
  for (let i = 0; i < n; i++) {
158
187
  sorted[i] = nodes[order[i]];
159
188
  }
160
189
  this.nodes = sorted;
161
- this.sync();
190
+ if (!parent || keep.size === n) {
191
+ return;
192
+ }
193
+ let ref = end;
194
+ for (let i = n - 1; i >= 0; i--) {
195
+ let group = sorted[i];
196
+ if (keep.has(i)) {
197
+ ref = group.head;
198
+ continue;
199
+ }
200
+ let node = group.tail;
201
+ while (node) {
202
+ let prev = node === group.head ? null : node.previousSibling;
203
+ parent.insertBefore(node, ref);
204
+ ref = node;
205
+ node = prev;
206
+ }
207
+ }
162
208
  }
163
209
  splice(start, stop = this.nodes.length, items) {
164
210
  if (!items.length) {
@@ -1,7 +1,6 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { ARRAY_SLOT } from '../constants.js';
3
- import { clone, fragment, text } from '../utilities.js';
4
- const EMPTY_FRAGMENT = fragment('');
3
+ import { clone, EMPTY_FRAGMENT, text } from '../utilities.js';
5
4
  export default function render(anchor, value) {
6
5
  if (value == null || value === false || value === '') {
7
6
  return EMPTY_FRAGMENT;
@@ -1,7 +1,8 @@
1
1
  declare const clone: <T extends Node>(node: T, options?: boolean | ImportNodeOptions) => T;
2
2
  declare const fragment: (html: string) => DocumentFragment;
3
+ declare const EMPTY_FRAGMENT: DocumentFragment;
3
4
  declare const marker: ChildNode;
4
5
  declare const raf: typeof requestAnimationFrame;
5
6
  declare const template: (html: string) => () => DocumentFragment;
6
7
  declare const text: (value: string) => Node;
7
- export { clone, fragment, template, marker, raf, text };
8
+ export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };
@@ -8,6 +8,7 @@ const fragment = (html) => {
8
8
  element.innerHTML = html;
9
9
  return element.content;
10
10
  };
11
+ const EMPTY_FRAGMENT = fragment('');
11
12
  const marker = fragment(SLOT_HTML).firstChild;
12
13
  const raf = globalThis?.requestAnimationFrame;
13
14
  const template = (html) => {
@@ -28,4 +29,4 @@ const text = (value) => {
28
29
  }
29
30
  return element;
30
31
  };
31
- export { clone, fragment, template, marker, raf, text };
32
+ export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };
package/package.json CHANGED
@@ -38,8 +38,9 @@
38
38
  },
39
39
  "type": "module",
40
40
  "types": "./build/index.d.ts",
41
- "version": "0.16.14",
41
+ "version": "0.16.15",
42
42
  "scripts": {
43
+ "bench:run": "vitest bench --config vitest.bench.config.ts",
43
44
  "build": "tsc",
44
45
  "test": "vitest run",
45
46
  "test:watch": "vitest",
package/src/attributes.ts CHANGED
@@ -29,7 +29,10 @@ function apply(element: Element, name: string, value: unknown) {
29
29
  else if (name === 'class') {
30
30
  element.className = value as string;
31
31
  }
32
- else if (name === 'style' || (name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
32
+ else if (name === 'style') {
33
+ element.style.cssText = value as string;
34
+ }
35
+ else if ((name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
33
36
  element.setAttribute(name, value as string);
34
37
  }
35
38
  else {
package/src/slot/array.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { read, root, signal, write, Reactive } from '@esportsplus/reactivity';
2
2
  import { ARRAY_SLOT } from '../constants';
3
3
  import { Element, SlotGroup } from '../types';
4
- import { clone, fragment, marker, raf } from '../utilities';
4
+ import { clone, EMPTY_FRAGMENT, marker, raf } from '../utilities';
5
5
  import { ondisconnect, remove } from './cleanup';
6
+
6
7
  import html from '../html';
7
8
 
8
9
 
@@ -19,7 +20,51 @@ type ArraySlotOp<T> =
19
20
  | { op: 'sort'; order: number[] };
20
21
 
21
22
 
22
- const EMPTY_FRAGMENT = fragment('');
23
+ function lis(arr: number[]): Set<number> {
24
+ let n = arr.length;
25
+
26
+ if (n === 0) {
27
+ return new Set();
28
+ }
29
+
30
+ let ends = new Int32Array(n),
31
+ predecessors = new Int32Array(n),
32
+ len = 0;
33
+
34
+ for (let i = 0; i < n; i++) {
35
+ let lo = 0,
36
+ hi = len,
37
+ val = arr[i];
38
+
39
+ while (lo < hi) {
40
+ let mid = (lo + hi) >> 1;
41
+
42
+ if (arr[ends[mid]] < val) {
43
+ lo = mid + 1;
44
+ }
45
+ else {
46
+ hi = mid;
47
+ }
48
+ }
49
+
50
+ ends[lo] = i;
51
+ predecessors[i] = lo > 0 ? ends[lo - 1] : -1;
52
+
53
+ if (lo >= len) {
54
+ len = lo + 1;
55
+ }
56
+ }
57
+
58
+ let idx = ends[len - 1],
59
+ result = new Set<number>();
60
+
61
+ for (let i = len - 1; i >= 0; i--) {
62
+ result.add(idx);
63
+ idx = predecessors[idx];
64
+ }
65
+
66
+ return result;
67
+ }
23
68
 
24
69
 
25
70
  class ArraySlot<T> {
@@ -204,14 +249,41 @@ class ArraySlot<T> {
204
249
  return;
205
250
  }
206
251
 
207
- let sorted = new Array(n) as SlotGroup[];
252
+ let end: Node | null = n > 0 ? nodes[n - 1].tail.nextSibling : null,
253
+ keep = lis(order),
254
+ parent = this.marker.parentNode,
255
+ sorted = new Array(n) as SlotGroup[];
208
256
 
209
257
  for (let i = 0; i < n; i++) {
210
258
  sorted[i] = nodes[order[i]];
211
259
  }
212
260
 
213
261
  this.nodes = sorted;
214
- this.sync();
262
+
263
+ if (!parent || keep.size === n) {
264
+ return;
265
+ }
266
+
267
+ let ref: Node | null = end;
268
+
269
+ for (let i = n - 1; i >= 0; i--) {
270
+ let group = sorted[i];
271
+
272
+ if (keep.has(i)) {
273
+ ref = group.head;
274
+ continue;
275
+ }
276
+
277
+ let node: Node | null = group.tail;
278
+
279
+ while (node) {
280
+ let prev: Node | null = node === group.head ? null : node.previousSibling;
281
+
282
+ parent.insertBefore(node, ref);
283
+ ref = node;
284
+ node = prev;
285
+ }
286
+ }
215
287
  }
216
288
 
217
289
  private splice(start: number, stop: number = this.nodes.length, items: T[]) {
@@ -1,13 +1,10 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { ARRAY_SLOT } from '../constants';
3
3
  import { Element } from '../types';
4
- import { clone, fragment, text } from '../utilities';
4
+ import { clone, EMPTY_FRAGMENT, text } from '../utilities';
5
5
  import { ArraySlot } from './array';
6
6
 
7
7
 
8
- const EMPTY_FRAGMENT = fragment('');
9
-
10
-
11
8
  export default function render(anchor: Element, value: unknown): Node {
12
9
  if (value == null || value === false || value === '') {
13
10
  return EMPTY_FRAGMENT;
package/src/utilities.ts CHANGED
@@ -19,6 +19,8 @@ const fragment = (html: string): DocumentFragment => {
19
19
  return element.content;
20
20
  };
21
21
 
22
+ const EMPTY_FRAGMENT = fragment('');
23
+
22
24
  const marker = fragment(SLOT_HTML).firstChild!;
23
25
 
24
26
  const raf = globalThis?.requestAnimationFrame;
@@ -50,4 +52,4 @@ const text = (value: string) => {
50
52
  };
51
53
 
52
54
 
53
- export { clone, fragment, template, marker, raf, text };
55
+ export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };