@esportsplus/template 0.16.15 → 0.17.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/README.md +81 -9
- package/build/compiler/plugins/vite.d.ts +8 -4
- package/build/compiler/plugins/vite.js +37 -2
- package/build/hmr.d.ts +10 -0
- package/build/hmr.js +42 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/slot/array.js +21 -2
- package/build/slot/effect.d.ts +3 -3
- package/build/slot/effect.js +36 -17
- package/llm.txt +90 -11
- package/package.json +1 -1
- package/src/compiler/plugins/vite.ts +74 -5
- package/src/hmr.ts +70 -0
- package/src/index.ts +1 -0
- package/src/slot/array.ts +30 -2
- package/src/slot/effect.ts +46 -20
- package/tests/attributes.ts +142 -2
- package/tests/compiler/codegen.ts +78 -0
- package/tests/compiler/transform.ts +159 -0
- package/tests/compiler/ts-analyzer.ts +128 -8
- package/tests/compiler/vite-hmr.ts +213 -0
- package/tests/event/index.ts +71 -0
- package/tests/hmr.ts +146 -0
- package/tests/slot/array.ts +201 -0
- package/tests/slot/async.ts +389 -0
- package/tests/slot/effect.ts +88 -1
- package/storage/feature-research-2026-03-24.md +0 -475
- package/test-output.txt +0 -0
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ High-performance, compiler-optimized HTML templating library for JavaScript/Type
|
|
|
9
9
|
- **Reactive integration** - Works with `@esportsplus/reactivity` for dynamic updates
|
|
10
10
|
- **Event delegation** - Efficient event handling with automatic delegation
|
|
11
11
|
- **Lifecycle events** - `onconnect`, `ondisconnect`, `onrender`, `onresize`, `ontick`
|
|
12
|
+
- **Async slots** - Async function support with fallback content in `EffectSlot`
|
|
13
|
+
- **Non-destructive reordering** - Uses `moveBefore` DOM API for array sort/reverse when available
|
|
14
|
+
- **HMR support** - Fine-grained hot module replacement for templates in development
|
|
12
15
|
- **Type-safe** - Full TypeScript support
|
|
13
16
|
|
|
14
17
|
## Installation
|
|
@@ -29,10 +32,12 @@ import { defineConfig } from 'vite';
|
|
|
29
32
|
import template from '@esportsplus/template/compiler/vite';
|
|
30
33
|
|
|
31
34
|
export default defineConfig({
|
|
32
|
-
plugins: [template]
|
|
35
|
+
plugins: [template()]
|
|
33
36
|
});
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
HMR is automatically enabled in development mode (`vite dev`). When a file containing `html` templates is saved, only the affected template factories are invalidated and re-created — no full page reload needed.
|
|
40
|
+
|
|
36
41
|
### TypeScript Compiler (tsc)
|
|
37
42
|
|
|
38
43
|
For direct `tsc` compilation, use the transformer:
|
|
@@ -179,6 +184,31 @@ const todoList = (todos: string[]) =>
|
|
|
179
184
|
html`<ul>${html.reactive(todos, todo => html`<li>${todo}</li>`)}</ul>`;
|
|
180
185
|
```
|
|
181
186
|
|
|
187
|
+
Array sort and reverse operations use the `moveBefore` DOM API when available, preserving element state (focus, animations, iframe content) during reordering. Falls back to `insertBefore` in older browsers.
|
|
188
|
+
|
|
189
|
+
### Async Slots
|
|
190
|
+
|
|
191
|
+
Async functions are supported in effect slots, with an optional fallback callback for loading states:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { html } from '@esportsplus/template';
|
|
195
|
+
|
|
196
|
+
// Async with fallback content while loading
|
|
197
|
+
const asyncContent = () =>
|
|
198
|
+
html`<div>${async (fallback: (content: any) => void) => {
|
|
199
|
+
fallback(html`<span>Loading...</span>`);
|
|
200
|
+
const data = await fetchData();
|
|
201
|
+
return html`<span>${data}</span>`;
|
|
202
|
+
}}</div>`;
|
|
203
|
+
|
|
204
|
+
// Simple async (no fallback)
|
|
205
|
+
const simpleAsync = () =>
|
|
206
|
+
html`<div>${async () => {
|
|
207
|
+
const data = await fetchData();
|
|
208
|
+
return html`<span>${data}</span>`;
|
|
209
|
+
}}</div>`;
|
|
210
|
+
```
|
|
211
|
+
|
|
182
212
|
## Events
|
|
183
213
|
|
|
184
214
|
### Standard DOM Events
|
|
@@ -235,8 +265,9 @@ Some events don't bubble and are attached directly:
|
|
|
235
265
|
|
|
236
266
|
- `onfocus`, `onblur`, `onfocusin`, `onfocusout`
|
|
237
267
|
- `onload`, `onerror`
|
|
268
|
+
- `onmouseenter`, `onmouseleave`
|
|
238
269
|
- `onplay`, `onpause`, `onended`, `ontimeupdate`
|
|
239
|
-
- `onscroll`
|
|
270
|
+
- `onscroll`, `onsubmit`, `onreset`
|
|
240
271
|
|
|
241
272
|
```typescript
|
|
242
273
|
const input = (focus: () => void, blur: () => void) =>
|
|
@@ -256,6 +287,20 @@ const circle = (fill: string) =>
|
|
|
256
287
|
</svg>`;
|
|
257
288
|
```
|
|
258
289
|
|
|
290
|
+
### SVG Sprites
|
|
291
|
+
|
|
292
|
+
Use `svg.sprite` to create `<use>` references to SVG sprite sheets:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { svg } from '@esportsplus/template';
|
|
296
|
+
|
|
297
|
+
// Generates <svg><use href="#icon-name" /></svg>
|
|
298
|
+
const icon = svg.sprite('icon-name');
|
|
299
|
+
|
|
300
|
+
// Hash prefix is added automatically if missing
|
|
301
|
+
const icon2 = svg.sprite('#icon-name');
|
|
302
|
+
```
|
|
303
|
+
|
|
259
304
|
## API Reference
|
|
260
305
|
|
|
261
306
|
### Exports
|
|
@@ -263,20 +308,47 @@ const circle = (fill: string) =>
|
|
|
263
308
|
| Export | Description |
|
|
264
309
|
|--------|-------------|
|
|
265
310
|
| `html` | Template literal tag for HTML |
|
|
266
|
-
| `svg` | Template literal tag for SVG |
|
|
311
|
+
| `svg` | Template literal tag for SVG (+ `svg.sprite()`) |
|
|
267
312
|
| `render` | Mount renderable to DOM element |
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
313
|
+
| `setList` | Set class/style list attributes with merge support |
|
|
314
|
+
| `setProperty` | Set a single element property/attribute |
|
|
315
|
+
| `setProperties` | Set multiple properties from an object |
|
|
316
|
+
| `delegate` | Register delegated event handler |
|
|
317
|
+
| `on` | Register direct-attach event handler |
|
|
318
|
+
| `onconnect` | Lifecycle: element connected to DOM |
|
|
319
|
+
| `ondisconnect` | Lifecycle: element disconnected from DOM |
|
|
320
|
+
| `onrender` | Lifecycle: after initial render |
|
|
321
|
+
| `onresize` | Lifecycle: window resize |
|
|
322
|
+
| `ontick` | Lifecycle: RAF animation loop |
|
|
323
|
+
| `runtime` | Route event name to correct handler |
|
|
324
|
+
| `slot` | Static slot rendering |
|
|
271
325
|
| `ArraySlot` | Reactive array rendering |
|
|
272
326
|
| `EffectSlot` | Reactive effect rendering |
|
|
327
|
+
| `clone` | Clone a node (uses `importNode` on Firefox) |
|
|
328
|
+
| `fragment` | Parse HTML string into DocumentFragment |
|
|
329
|
+
| `marker` | Slot position comment node |
|
|
330
|
+
| `template` | Create cached template factory |
|
|
331
|
+
| `text` | Create text node |
|
|
332
|
+
| `raf` | `requestAnimationFrame` reference |
|
|
333
|
+
| `accept` | HMR accept handler (dev only) |
|
|
334
|
+
| `createHotTemplate` | HMR template factory (dev only) |
|
|
335
|
+
| `hmrReset` | HMR state reset (test only) |
|
|
273
336
|
|
|
274
337
|
### Types
|
|
275
338
|
|
|
276
339
|
```typescript
|
|
277
|
-
type Renderable = DocumentFragment |
|
|
278
|
-
type Element = HTMLElement &
|
|
279
|
-
type Attributes =
|
|
340
|
+
type Renderable<T> = ArraySlot<T> | DocumentFragment | Effect<T> | Node | NodeList | Primitive | Renderable<T>[];
|
|
341
|
+
type Element = HTMLElement & Attributes<any>;
|
|
342
|
+
type Attributes<T extends HTMLElement = Element> = {
|
|
343
|
+
class?: Attribute | Attribute[];
|
|
344
|
+
style?: Attribute | Attribute[];
|
|
345
|
+
onconnect?: (element: T) => void;
|
|
346
|
+
ondisconnect?: (element: T) => void;
|
|
347
|
+
onrender?: (element: T) => void;
|
|
348
|
+
ontick?: (dispose: VoidFunction, element: T) => void;
|
|
349
|
+
[key: `aria-${string}`]: string | number | boolean | undefined;
|
|
350
|
+
[key: `data-${string}`]: string | undefined;
|
|
351
|
+
} & { [K in keyof GlobalEventHandlersEventMap as `on${string & K}`]?: (this: T, event: GlobalEventHandlersEventMap[K]) => void };
|
|
280
352
|
```
|
|
281
353
|
|
|
282
354
|
### render(parent, renderable)
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
declare const _default: ({ root }?: {
|
|
2
2
|
root?: string;
|
|
3
3
|
}) => {
|
|
4
|
-
configResolved
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
configResolved(config: any): void;
|
|
5
|
+
handleHotUpdate(_ctx: {
|
|
6
|
+
file: string;
|
|
7
|
+
modules: any[];
|
|
8
|
+
}): void;
|
|
9
|
+
transform(code: string, id: string): {
|
|
8
10
|
code: string;
|
|
9
11
|
map: null;
|
|
10
12
|
} | null;
|
|
13
|
+
enforce: "pre";
|
|
14
|
+
name: string;
|
|
11
15
|
watchChange: (id: string) => void;
|
|
12
16
|
};
|
|
13
17
|
export default _default;
|
|
@@ -1,8 +1,43 @@
|
|
|
1
|
+
import { NAMESPACE, PACKAGE_NAME } from '../constants.js';
|
|
1
2
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import { PACKAGE_NAME } from '../constants.js';
|
|
3
3
|
import reactivity from '@esportsplus/reactivity/compiler';
|
|
4
4
|
import template from '../index.js';
|
|
5
|
-
|
|
5
|
+
const TEMPLATE_SEARCH = NAMESPACE + '.template(';
|
|
6
|
+
const TEMPLATE_CALL_REGEX = new RegExp('(const\\s+(\\w+)\\s*=\\s*' + NAMESPACE + '\\.template\\()(`)', 'g');
|
|
7
|
+
let base = plugin.vite({
|
|
6
8
|
name: PACKAGE_NAME,
|
|
7
9
|
plugins: [reactivity, template]
|
|
8
10
|
});
|
|
11
|
+
function injectHMR(code, id) {
|
|
12
|
+
let hmrId = id.replace(/\\/g, '/'), hotReplace = NAMESPACE + '.createHotTemplate("' + hmrId + '", "', injected = code.replace(TEMPLATE_CALL_REGEX, function (_match, prefix, varName, backtick) {
|
|
13
|
+
return prefix.replace(TEMPLATE_SEARCH, hotReplace + varName + '", ') + backtick;
|
|
14
|
+
});
|
|
15
|
+
if (injected === code) {
|
|
16
|
+
return code;
|
|
17
|
+
}
|
|
18
|
+
injected += '\nif (import.meta.hot) { import.meta.hot.accept(() => { ' + NAMESPACE + '.accept("' + hmrId + '"); }); }';
|
|
19
|
+
return injected;
|
|
20
|
+
}
|
|
21
|
+
export default ({ root } = {}) => {
|
|
22
|
+
let isDev = false, vitePlugin = base({ root });
|
|
23
|
+
return {
|
|
24
|
+
...vitePlugin,
|
|
25
|
+
configResolved(config) {
|
|
26
|
+
vitePlugin.configResolved(config);
|
|
27
|
+
isDev = config?.command === 'serve' || config?.mode === 'development';
|
|
28
|
+
},
|
|
29
|
+
handleHotUpdate(_ctx) {
|
|
30
|
+
},
|
|
31
|
+
transform(code, id) {
|
|
32
|
+
let result = vitePlugin.transform(code, id);
|
|
33
|
+
if (!result || !isDev) {
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
let injected = injectHMR(result.code, id);
|
|
37
|
+
if (injected === result.code) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
return { code: injected, map: null };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
package/build/hmr.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare let modules: Map<string, Map<string, HotTemplate>>;
|
|
2
|
+
type HotTemplate = {
|
|
3
|
+
cached: DocumentFragment | undefined;
|
|
4
|
+
factory: () => DocumentFragment;
|
|
5
|
+
html: string;
|
|
6
|
+
};
|
|
7
|
+
declare const accept: (moduleId: string) => void;
|
|
8
|
+
declare const createHotTemplate: (moduleId: string, templateId: string, html: string) => () => DocumentFragment;
|
|
9
|
+
declare const hmrReset: () => void;
|
|
10
|
+
export { accept, createHotTemplate, hmrReset, modules };
|
package/build/hmr.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
let clone = (node, deep = true) => node.cloneNode(deep), modules = new Map(), tmpl = typeof document !== 'undefined' ? document.createElement('template') : null;
|
|
2
|
+
function invalidate(moduleId) {
|
|
3
|
+
let templates = modules.get(moduleId);
|
|
4
|
+
if (!templates) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
for (let [, entry] of templates) {
|
|
8
|
+
entry.cached = undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function register(moduleId, templateId, html) {
|
|
12
|
+
let entry = {
|
|
13
|
+
cached: undefined,
|
|
14
|
+
factory: () => {
|
|
15
|
+
if (!entry.cached) {
|
|
16
|
+
let element = tmpl.cloneNode();
|
|
17
|
+
element.innerHTML = entry.html;
|
|
18
|
+
entry.cached = element.content;
|
|
19
|
+
}
|
|
20
|
+
return clone(entry.cached, true);
|
|
21
|
+
},
|
|
22
|
+
html
|
|
23
|
+
};
|
|
24
|
+
(modules.get(moduleId) ?? (modules.set(moduleId, new Map()), modules.get(moduleId))).set(templateId, entry);
|
|
25
|
+
return entry.factory;
|
|
26
|
+
}
|
|
27
|
+
const accept = (moduleId) => {
|
|
28
|
+
invalidate(moduleId);
|
|
29
|
+
};
|
|
30
|
+
const createHotTemplate = (moduleId, templateId, html) => {
|
|
31
|
+
let existing = modules.get(moduleId)?.get(templateId);
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.cached = undefined;
|
|
34
|
+
existing.html = html;
|
|
35
|
+
return existing.factory;
|
|
36
|
+
}
|
|
37
|
+
return register(moduleId, templateId, html);
|
|
38
|
+
};
|
|
39
|
+
const hmrReset = () => {
|
|
40
|
+
modules.clear();
|
|
41
|
+
};
|
|
42
|
+
export { accept, createHotTemplate, hmrReset, modules };
|
package/build/index.d.ts
CHANGED
package/build/index.js
CHANGED
|
@@ -5,6 +5,7 @@ if (typeof Node !== 'undefined') {
|
|
|
5
5
|
}
|
|
6
6
|
export * from './attributes.js';
|
|
7
7
|
export * from './event/index.js';
|
|
8
|
+
export * from './hmr.js';
|
|
8
9
|
export * from './utilities.js';
|
|
9
10
|
export { default as html } from './html.js';
|
|
10
11
|
export { default as render } from './render.js';
|
package/build/slot/array.js
CHANGED
|
@@ -190,7 +190,7 @@ class ArraySlot {
|
|
|
190
190
|
if (!parent || keep.size === n) {
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
|
-
let ref = end;
|
|
193
|
+
let ref = end, useMoveBefore = 'moveBefore' in parent;
|
|
194
194
|
for (let i = n - 1; i >= 0; i--) {
|
|
195
195
|
let group = sorted[i];
|
|
196
196
|
if (keep.has(i)) {
|
|
@@ -200,7 +200,12 @@ class ArraySlot {
|
|
|
200
200
|
let node = group.tail;
|
|
201
201
|
while (node) {
|
|
202
202
|
let prev = node === group.head ? null : node.previousSibling;
|
|
203
|
-
|
|
203
|
+
if (useMoveBefore) {
|
|
204
|
+
parent.moveBefore(node, ref);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
parent.insertBefore(node, ref);
|
|
208
|
+
}
|
|
204
209
|
ref = node;
|
|
205
210
|
node = prev;
|
|
206
211
|
}
|
|
@@ -219,6 +224,20 @@ class ArraySlot {
|
|
|
219
224
|
if (!n) {
|
|
220
225
|
return;
|
|
221
226
|
}
|
|
227
|
+
let parent = this.marker.parentNode;
|
|
228
|
+
if (parent && 'moveBefore' in parent) {
|
|
229
|
+
let ref = nodes[0].tail.nextSibling;
|
|
230
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
231
|
+
let group = nodes[i], node = group.tail;
|
|
232
|
+
while (node) {
|
|
233
|
+
let prev = node === group.head ? null : node.previousSibling;
|
|
234
|
+
parent.moveBefore(node, ref);
|
|
235
|
+
ref = node;
|
|
236
|
+
node = prev;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
222
241
|
for (let i = 0; i < n; i++) {
|
|
223
242
|
let group = nodes[i], next, node = group.head;
|
|
224
243
|
while (node) {
|
package/build/slot/effect.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Element,
|
|
1
|
+
import { Element, SlotGroup } from '../types.js';
|
|
2
2
|
declare class EffectSlot {
|
|
3
3
|
anchor: Element;
|
|
4
|
-
disposer: VoidFunction;
|
|
4
|
+
disposer: VoidFunction | null;
|
|
5
5
|
group: SlotGroup | null;
|
|
6
6
|
scheduled: boolean;
|
|
7
7
|
textnode: Node | null;
|
|
8
|
-
constructor(anchor: Element, fn: (
|
|
8
|
+
constructor(anchor: Element, fn: ((...args: any[]) => any));
|
|
9
9
|
dispose(): void;
|
|
10
10
|
update(value: unknown): void;
|
|
11
11
|
}
|
package/build/slot/effect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effect } from '@esportsplus/reactivity';
|
|
2
|
+
import { isAsyncFunction } from '@esportsplus/utilities';
|
|
2
3
|
import { raf, text } from '../utilities.js';
|
|
3
4
|
import { remove } from './cleanup.js';
|
|
4
5
|
import render from './render.js';
|
|
@@ -18,37 +19,47 @@ class EffectSlot {
|
|
|
18
19
|
scheduled = false;
|
|
19
20
|
textnode = null;
|
|
20
21
|
constructor(anchor, fn) {
|
|
21
|
-
let dispose = fn.length ? () => this.dispose() : undefined, value;
|
|
22
22
|
this.anchor = anchor;
|
|
23
|
-
this.disposer =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
this.disposer = null;
|
|
24
|
+
if (isAsyncFunction(fn)) {
|
|
25
|
+
fn((content) => this.update(content)).then((value) => this.update(value), () => { });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
let dispose = fn.length ? () => this.dispose() : undefined, value;
|
|
29
|
+
this.disposer = effect(() => {
|
|
30
|
+
value = read(fn(dispose));
|
|
31
|
+
if (!this.disposer) {
|
|
32
32
|
this.update(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
}
|
|
34
|
+
else if (!this.scheduled) {
|
|
35
|
+
this.scheduled = true;
|
|
36
|
+
raf(() => {
|
|
37
|
+
this.scheduled = false;
|
|
38
|
+
this.update(value);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
dispose() {
|
|
38
|
-
let { anchor, group, textnode } = this;
|
|
45
|
+
let { anchor, disposer, group, textnode } = this;
|
|
46
|
+
if (!disposer) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
39
49
|
if (textnode) {
|
|
40
50
|
group = { head: anchor, tail: textnode };
|
|
41
51
|
}
|
|
42
52
|
else if (group) {
|
|
43
53
|
group.head = anchor;
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
disposer();
|
|
46
56
|
if (group) {
|
|
47
57
|
remove(group);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
update(value) {
|
|
51
61
|
let { anchor, group, textnode } = this;
|
|
62
|
+
value = read(value);
|
|
52
63
|
if (group) {
|
|
53
64
|
remove(group);
|
|
54
65
|
this.group = null;
|
|
@@ -68,14 +79,22 @@ class EffectSlot {
|
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
else {
|
|
71
|
-
let fragment = render(anchor, value), head
|
|
82
|
+
let fragment = render(anchor, value), head, tail;
|
|
83
|
+
if (fragment.nodeType === 11) {
|
|
84
|
+
head = fragment.firstChild;
|
|
85
|
+
tail = fragment.lastChild;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
head = fragment;
|
|
89
|
+
tail = fragment;
|
|
90
|
+
}
|
|
72
91
|
if (textnode?.isConnected) {
|
|
73
92
|
remove({ head: textnode, tail: textnode });
|
|
74
93
|
}
|
|
75
94
|
if (head) {
|
|
76
95
|
this.group = {
|
|
77
96
|
head: head,
|
|
78
|
-
tail:
|
|
97
|
+
tail: tail
|
|
79
98
|
};
|
|
80
99
|
anchor.after(fragment);
|
|
81
100
|
}
|
package/llm.txt
CHANGED
|
@@ -30,9 +30,16 @@ A compile-time template system that transforms `html` tagged template literals i
|
|
|
30
30
|
│ RUNTIME │
|
|
31
31
|
├─────────────────────────────────────────────────────────────────┤
|
|
32
32
|
│ utilities.ts → template(), clone(), fragment() │
|
|
33
|
+
│ hmr.ts → createHotTemplate(), accept() (dev only) │
|
|
33
34
|
│ attributes.ts → setList(), setProperty(), reactive bindings │
|
|
34
35
|
│ slot/ → ArraySlot, EffectSlot, static rendering │
|
|
35
36
|
│ event/ → Event delegation, lifecycle hooks │
|
|
37
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ VITE PLUGIN (DEV) │
|
|
39
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
40
|
+
│ plugins/vite.ts → Wraps base transform + injects HMR code │
|
|
41
|
+
│ In dev: template() → createHotTemplate() │
|
|
42
|
+
│ Appends import.meta.hot.accept() │
|
|
36
43
|
└─────────────────────────────────────────────────────────────────┘
|
|
37
44
|
```
|
|
38
45
|
|
|
@@ -153,6 +160,19 @@ Analyzes expressions to determine optimal runtime binding:
|
|
|
153
160
|
|
|
154
161
|
## Runtime Components
|
|
155
162
|
|
|
163
|
+
### Entry Point: `index.ts`
|
|
164
|
+
|
|
165
|
+
Pre-allocates `CLEANUP` and `STORE` symbol properties on `Node.prototype` to optimize property access (avoids hidden class transitions when first set per-element):
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
if (typeof Node !== 'undefined') {
|
|
169
|
+
(Node.prototype as any)[CLEANUP] = null;
|
|
170
|
+
(Node.prototype as any)[STORE] = null;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Re-exports everything from: `attributes`, `event`, `hmr`, `utilities`, `html`, `render`, `slot` (including `ArraySlot`, `EffectSlot`), `svg`, and types.
|
|
175
|
+
|
|
156
176
|
### Template Factory: `utilities.ts`
|
|
157
177
|
|
|
158
178
|
```typescript
|
|
@@ -170,6 +190,8 @@ const template = (html: string) => {
|
|
|
170
190
|
|
|
171
191
|
Caches parsed HTML, returns cloned fragments on each call.
|
|
172
192
|
|
|
193
|
+
**Additional exports**: `clone` (uses `importNode` on Firefox for performance, `cloneNode` elsewhere), `EMPTY_FRAGMENT` (cached empty fragment), `fragment(html)` (parse HTML string), `marker` (slot comment node `<!--$-->`), `raf` (bound `requestAnimationFrame`), `text(value)` (create text node).
|
|
194
|
+
|
|
173
195
|
### Slot System: `slot/`
|
|
174
196
|
|
|
175
197
|
**Three slot types**:
|
|
@@ -182,11 +204,17 @@ Caches parsed HTML, returns cloned fragments on each call.
|
|
|
182
204
|
- Wraps reactive function in `effect()`
|
|
183
205
|
- Updates content on dependency changes
|
|
184
206
|
- Batches updates via `requestAnimationFrame`
|
|
207
|
+
- **Async function support**: detects async functions via `isAsyncFunction()` from `@esportsplus/utilities`
|
|
208
|
+
- Async functions receive a `fallback` callback for loading state, resolved value replaces content
|
|
209
|
+
- Async signature: `async (fallback: (content: Renderable) => void) => Promise<Renderable>`
|
|
185
210
|
|
|
186
211
|
3. **ArraySlot** (`slot/array.ts`)
|
|
187
212
|
- Handles reactive arrays with fine-grained updates
|
|
188
|
-
- Listens to array mutation events (push, pop, splice, etc.)
|
|
213
|
+
- Listens to array mutation events (push, pop, splice, sort, reverse, etc.)
|
|
189
214
|
- Maintains DOM node groups per array item
|
|
215
|
+
- **`moveBefore` DOM API**: sort/reverse use `moveBefore` when available for non-destructive reordering (preserves focus, animations, iframe state)
|
|
216
|
+
- Falls back to `insertBefore` in browsers without `moveBefore`
|
|
217
|
+
- **LIS algorithm**: sort uses longest increasing subsequence to minimize DOM moves
|
|
190
218
|
|
|
191
219
|
**SlotGroup Structure**:
|
|
192
220
|
|
|
@@ -263,17 +291,22 @@ host.addEventListener(event, (e) => {
|
|
|
263
291
|
Events that don't bubble properly use `addEventListener` directly:
|
|
264
292
|
- `blur`, `focus`, `focusin`, `focusout`
|
|
265
293
|
- `load`, `error`
|
|
294
|
+
- `mouseenter`, `mouseleave`
|
|
266
295
|
- Media events: `play`, `pause`, `ended`, `timeupdate`
|
|
267
296
|
- `scroll`, `submit`, `reset`
|
|
268
297
|
|
|
269
298
|
**Lifecycle Events**:
|
|
270
299
|
|
|
271
300
|
Custom events handled specially:
|
|
272
|
-
- `onconnect` -
|
|
273
|
-
- `ondisconnect` -
|
|
274
|
-
- `onrender` -
|
|
275
|
-
- `onresize` -
|
|
276
|
-
- `ontick` - RAF-based animation loop
|
|
301
|
+
- `onconnect` - Polls `element.isConnected` via RAF loop (using `ontick`'s `add`/`remove`). Retries up to 60 frames, fires listener once when connected, then removes itself.
|
|
302
|
+
- `ondisconnect` - Registers cleanup function via `slot/cleanup.ts` CLEANUP symbol array
|
|
303
|
+
- `onrender` - Calls listener synchronously inside `root()` reactive scope
|
|
304
|
+
- `onresize` - Registers on global `window.resize` event. Tracks elements in a Map, auto-removes disconnected elements, removes window listener when no elements remain.
|
|
305
|
+
- `ontick` - RAF-based animation loop. Listener receives `(dispose, element)`. Auto-removes when element disconnects. 60-frame retry for initial connection.
|
|
306
|
+
|
|
307
|
+
### SVG: `svg.ts`
|
|
308
|
+
|
|
309
|
+
`svg` is bound to the same `html` tag function. Additionally provides `svg.sprite(href)` which creates an `<svg><use href="..." /></svg>` fragment from a cached template. Auto-prepends `#` to href if missing.
|
|
277
310
|
|
|
278
311
|
### Cleanup System: `slot/cleanup.ts`
|
|
279
312
|
|
|
@@ -291,6 +324,41 @@ const remove = (...groups) => {
|
|
|
291
324
|
};
|
|
292
325
|
```
|
|
293
326
|
|
|
327
|
+
### HMR Module: `hmr.ts`
|
|
328
|
+
|
|
329
|
+
**Purpose**: Dev-only runtime registry for hot module replacement of templates.
|
|
330
|
+
|
|
331
|
+
**Key Concepts**:
|
|
332
|
+
- In production, templates use `utilities.template()` which caches a parsed fragment and returns clones
|
|
333
|
+
- In development, the Vite plugin replaces `template()` calls with `createHotTemplate()` which registers factories in a module→template registry
|
|
334
|
+
- On file save, Vite triggers HMR accept which calls `accept(moduleId)` to invalidate cached fragments
|
|
335
|
+
- Next factory call re-parses the updated HTML, producing fresh DOM without page reload
|
|
336
|
+
|
|
337
|
+
**API**:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
// Register or update a template factory for HMR tracking
|
|
341
|
+
createHotTemplate(moduleId: string, templateId: string, html: string): () => DocumentFragment
|
|
342
|
+
|
|
343
|
+
// Invalidate all cached templates for a module (called from import.meta.hot.accept)
|
|
344
|
+
accept(moduleId: string): void
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Registry Structure**: `Map<moduleId, Map<templateId, HotTemplate>>` where HotTemplate holds the html string, cached fragment, and factory closure.
|
|
348
|
+
|
|
349
|
+
### Vite Plugin: `compiler/plugins/vite.ts`
|
|
350
|
+
|
|
351
|
+
**Purpose**: Wraps the base `plugin.vite()` transform with HMR injection.
|
|
352
|
+
|
|
353
|
+
**How it works**:
|
|
354
|
+
1. `configResolved` captures dev mode (`command === 'serve'` or `mode === 'development'`)
|
|
355
|
+
2. In dev mode, after the base transform runs, `injectHMR()` post-processes the output:
|
|
356
|
+
- Replaces `NAMESPACE.template(\`...\`)` → `NAMESPACE.createHotTemplate(moduleId, templateId, \`...\`)`
|
|
357
|
+
- Appends `import.meta.hot.accept(() => { NAMESPACE.accept(moduleId); })`
|
|
358
|
+
3. In production, no HMR code is injected — identical to previous behavior
|
|
359
|
+
|
|
360
|
+
**Regex**: `TEMPLATE_CALL_REGEX` matches `const varName = NAMESPACE.template(` to capture the variable name as the templateId.
|
|
361
|
+
|
|
294
362
|
## Constants Reference
|
|
295
363
|
|
|
296
364
|
### `constants.ts` (Runtime)
|
|
@@ -330,6 +398,11 @@ const remove = (...groups) => {
|
|
|
330
398
|
- Add to `DIRECT_ATTACH_EVENTS` set
|
|
331
399
|
- Uses existing `on()` function
|
|
332
400
|
|
|
401
|
+
4. **Modifying HMR behavior**
|
|
402
|
+
- `hmr.ts` is standalone with no internal deps — safe to modify
|
|
403
|
+
- `injectHMR()` in `plugins/vite.ts` only runs in dev mode
|
|
404
|
+
- Production builds are unaffected by HMR changes
|
|
405
|
+
|
|
333
406
|
### Dangerous Changes
|
|
334
407
|
|
|
335
408
|
1. **Modifying `parser.ts` level tracking**
|
|
@@ -345,6 +418,11 @@ const remove = (...groups) => {
|
|
|
345
418
|
- Slots must be processed in document order
|
|
346
419
|
- Expression indices must match slot order
|
|
347
420
|
|
|
421
|
+
4. **Modifying `slot/array.ts` sort/sync**
|
|
422
|
+
- Uses `moveBefore` API with `insertBefore` fallback — both paths must stay in sync
|
|
423
|
+
- LIS algorithm in `lis()` determines which nodes to keep in place; incorrect LIS = unnecessary moves
|
|
424
|
+
- `sync()` and `sort()` both have moveBefore/insertBefore branches
|
|
425
|
+
|
|
348
426
|
### Testing Recommendations
|
|
349
427
|
|
|
350
428
|
After modifying parser/codegen:
|
|
@@ -373,6 +451,7 @@ After modifying parser/codegen:
|
|
|
373
451
|
index.ts (runtime entry)
|
|
374
452
|
├── constants.ts
|
|
375
453
|
├── attributes.ts ← constants, types, utilities, event
|
|
454
|
+
├── hmr.ts (standalone, no internal deps)
|
|
376
455
|
├── html.ts ← types, slot (stub, replaced at compile)
|
|
377
456
|
├── render.ts ← types, utilities, slot
|
|
378
457
|
├── svg.ts (same as html.ts)
|
|
@@ -381,13 +460,13 @@ index.ts (runtime entry)
|
|
|
381
460
|
├── slot/
|
|
382
461
|
│ ├── index.ts ← types, effect, render
|
|
383
462
|
│ ├── array.ts ← reactivity, constants, types, utilities, cleanup, html
|
|
384
|
-
│ ├── effect.ts ← reactivity, types, utilities, cleanup, render
|
|
463
|
+
│ ├── effect.ts ← reactivity, utilities (isAsyncFunction), types, utilities, cleanup, render
|
|
385
464
|
│ ├── cleanup.ts ← constants, types
|
|
386
465
|
│ └── render.ts ← utilities, constants, types, array
|
|
387
466
|
└── event/
|
|
388
467
|
├── index.ts ← reactivity, utilities, constants, slot, types, onconnect/resize/tick
|
|
389
|
-
├── onconnect.ts ← types
|
|
390
|
-
├── onresize.ts ←
|
|
468
|
+
├── onconnect.ts ← types, ontick (add/remove)
|
|
469
|
+
├── onresize.ts ← reactivity (onCleanup), types
|
|
391
470
|
└── ontick.ts ← types, cleanup, utilities
|
|
392
471
|
|
|
393
472
|
compiler/
|
|
@@ -398,6 +477,6 @@ compiler/
|
|
|
398
477
|
├── ts-parser.ts ← typescript, typescript/compiler, constants
|
|
399
478
|
├── ts-analyzer.ts ← typescript, constants
|
|
400
479
|
└── plugins/
|
|
401
|
-
├── vite.ts ← typescript/compiler, constants, reactivity/compiler, ..
|
|
402
|
-
└── tsc.ts (similar)
|
|
480
|
+
├── vite.ts ← typescript/compiler, constants, reactivity/compiler, .. (wraps base plugin, adds HMR injection)
|
|
481
|
+
└── tsc.ts (similar, no HMR)
|
|
403
482
|
```
|