@esportsplus/template 0.16.15 → 0.17.1
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 +33 -1
- 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 +63 -4
- 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/compiler/vite-hmr.ts +126 -0
- package/tests/hmr.ts +146 -0
- package/tests/slot/array.ts +201 -0
- package/tests/slot/async.ts +389 -0
- 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
|
|
@@ -270,6 +300,8 @@ const circle = (fill: string) =>
|
|
|
270
300
|
| `slot` | Slot rendering |
|
|
271
301
|
| `ArraySlot` | Reactive array rendering |
|
|
272
302
|
| `EffectSlot` | Reactive effect rendering |
|
|
303
|
+
| `accept` | HMR accept handler (dev only) |
|
|
304
|
+
| `createHotTemplate` | HMR template factory (dev only) |
|
|
273
305
|
|
|
274
306
|
### Types
|
|
275
307
|
|
|
@@ -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
|
|
|
@@ -182,11 +189,17 @@ Caches parsed HTML, returns cloned fragments on each call.
|
|
|
182
189
|
- Wraps reactive function in `effect()`
|
|
183
190
|
- Updates content on dependency changes
|
|
184
191
|
- Batches updates via `requestAnimationFrame`
|
|
192
|
+
- **Async function support**: detects async functions via `isAsyncFunction()` from `@esportsplus/utilities`
|
|
193
|
+
- Async functions receive a `fallback` callback for loading state, resolved value replaces content
|
|
194
|
+
- Async signature: `async (fallback: (content: Renderable) => void) => Promise<Renderable>`
|
|
185
195
|
|
|
186
196
|
3. **ArraySlot** (`slot/array.ts`)
|
|
187
197
|
- Handles reactive arrays with fine-grained updates
|
|
188
|
-
- Listens to array mutation events (push, pop, splice, etc.)
|
|
198
|
+
- Listens to array mutation events (push, pop, splice, sort, reverse, etc.)
|
|
189
199
|
- Maintains DOM node groups per array item
|
|
200
|
+
- **`moveBefore` DOM API**: sort/reverse use `moveBefore` when available for non-destructive reordering (preserves focus, animations, iframe state)
|
|
201
|
+
- Falls back to `insertBefore` in browsers without `moveBefore`
|
|
202
|
+
- **LIS algorithm**: sort uses longest increasing subsequence to minimize DOM moves
|
|
190
203
|
|
|
191
204
|
**SlotGroup Structure**:
|
|
192
205
|
|
|
@@ -291,6 +304,41 @@ const remove = (...groups) => {
|
|
|
291
304
|
};
|
|
292
305
|
```
|
|
293
306
|
|
|
307
|
+
### HMR Module: `hmr.ts`
|
|
308
|
+
|
|
309
|
+
**Purpose**: Dev-only runtime registry for hot module replacement of templates.
|
|
310
|
+
|
|
311
|
+
**Key Concepts**:
|
|
312
|
+
- In production, templates use `utilities.template()` which caches a parsed fragment and returns clones
|
|
313
|
+
- In development, the Vite plugin replaces `template()` calls with `createHotTemplate()` which registers factories in a module→template registry
|
|
314
|
+
- On file save, Vite triggers HMR accept which calls `accept(moduleId)` to invalidate cached fragments
|
|
315
|
+
- Next factory call re-parses the updated HTML, producing fresh DOM without page reload
|
|
316
|
+
|
|
317
|
+
**API**:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Register or update a template factory for HMR tracking
|
|
321
|
+
createHotTemplate(moduleId: string, templateId: string, html: string): () => DocumentFragment
|
|
322
|
+
|
|
323
|
+
// Invalidate all cached templates for a module (called from import.meta.hot.accept)
|
|
324
|
+
accept(moduleId: string): void
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Registry Structure**: `Map<moduleId, Map<templateId, HotTemplate>>` where HotTemplate holds the html string, cached fragment, and factory closure.
|
|
328
|
+
|
|
329
|
+
### Vite Plugin: `compiler/plugins/vite.ts`
|
|
330
|
+
|
|
331
|
+
**Purpose**: Wraps the base `plugin.vite()` transform with HMR injection.
|
|
332
|
+
|
|
333
|
+
**How it works**:
|
|
334
|
+
1. `configResolved` captures dev mode (`command === 'serve'` or `mode === 'development'`)
|
|
335
|
+
2. In dev mode, after the base transform runs, `injectHMR()` post-processes the output:
|
|
336
|
+
- Replaces `NAMESPACE.template(\`...\`)` → `NAMESPACE.createHotTemplate(moduleId, templateId, \`...\`)`
|
|
337
|
+
- Appends `import.meta.hot.accept(() => { NAMESPACE.accept(moduleId); })`
|
|
338
|
+
3. In production, no HMR code is injected — identical to previous behavior
|
|
339
|
+
|
|
340
|
+
**Regex**: `TEMPLATE_CALL_REGEX` matches `const varName = NAMESPACE.template(` to capture the variable name as the templateId.
|
|
341
|
+
|
|
294
342
|
## Constants Reference
|
|
295
343
|
|
|
296
344
|
### `constants.ts` (Runtime)
|
|
@@ -330,6 +378,11 @@ const remove = (...groups) => {
|
|
|
330
378
|
- Add to `DIRECT_ATTACH_EVENTS` set
|
|
331
379
|
- Uses existing `on()` function
|
|
332
380
|
|
|
381
|
+
4. **Modifying HMR behavior**
|
|
382
|
+
- `hmr.ts` is standalone with no internal deps — safe to modify
|
|
383
|
+
- `injectHMR()` in `plugins/vite.ts` only runs in dev mode
|
|
384
|
+
- Production builds are unaffected by HMR changes
|
|
385
|
+
|
|
333
386
|
### Dangerous Changes
|
|
334
387
|
|
|
335
388
|
1. **Modifying `parser.ts` level tracking**
|
|
@@ -345,6 +398,11 @@ const remove = (...groups) => {
|
|
|
345
398
|
- Slots must be processed in document order
|
|
346
399
|
- Expression indices must match slot order
|
|
347
400
|
|
|
401
|
+
4. **Modifying `slot/array.ts` sort/sync**
|
|
402
|
+
- Uses `moveBefore` API with `insertBefore` fallback — both paths must stay in sync
|
|
403
|
+
- LIS algorithm in `lis()` determines which nodes to keep in place; incorrect LIS = unnecessary moves
|
|
404
|
+
- `sync()` and `sort()` both have moveBefore/insertBefore branches
|
|
405
|
+
|
|
348
406
|
### Testing Recommendations
|
|
349
407
|
|
|
350
408
|
After modifying parser/codegen:
|
|
@@ -373,6 +431,7 @@ After modifying parser/codegen:
|
|
|
373
431
|
index.ts (runtime entry)
|
|
374
432
|
├── constants.ts
|
|
375
433
|
├── attributes.ts ← constants, types, utilities, event
|
|
434
|
+
├── hmr.ts (standalone, no internal deps)
|
|
376
435
|
├── html.ts ← types, slot (stub, replaced at compile)
|
|
377
436
|
├── render.ts ← types, utilities, slot
|
|
378
437
|
├── svg.ts (same as html.ts)
|
|
@@ -381,7 +440,7 @@ index.ts (runtime entry)
|
|
|
381
440
|
├── slot/
|
|
382
441
|
│ ├── index.ts ← types, effect, render
|
|
383
442
|
│ ├── array.ts ← reactivity, constants, types, utilities, cleanup, html
|
|
384
|
-
│ ├── effect.ts ← reactivity, types, utilities, cleanup, render
|
|
443
|
+
│ ├── effect.ts ← reactivity, utilities (isAsyncFunction), types, utilities, cleanup, render
|
|
385
444
|
│ ├── cleanup.ts ← constants, types
|
|
386
445
|
│ └── render.ts ← utilities, constants, types, array
|
|
387
446
|
└── event/
|
|
@@ -398,6 +457,6 @@ compiler/
|
|
|
398
457
|
├── ts-parser.ts ← typescript, typescript/compiler, constants
|
|
399
458
|
├── ts-analyzer.ts ← typescript, constants
|
|
400
459
|
└── plugins/
|
|
401
|
-
├── vite.ts ← typescript/compiler, constants, reactivity/compiler, ..
|
|
402
|
-
└── tsc.ts (similar)
|
|
460
|
+
├── vite.ts ← typescript/compiler, constants, reactivity/compiler, .. (wraps base plugin, adds HMR injection)
|
|
461
|
+
└── tsc.ts (similar, no HMR)
|
|
403
462
|
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,79 @@
|
|
|
1
|
+
import { NAMESPACE, PACKAGE_NAME } from '../constants';
|
|
1
2
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
|
|
3
|
+
|
|
3
4
|
import reactivity from '@esportsplus/reactivity/compiler';
|
|
4
5
|
import template from '..';
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
8
|
+
type VitePlugin = {
|
|
9
|
+
configResolved: (config: any) => void;
|
|
10
|
+
enforce: 'pre';
|
|
11
|
+
handleHotUpdate?: (ctx: { file: string; modules: any[] }) => void;
|
|
12
|
+
name: string;
|
|
13
|
+
transform: (code: string, id: string) => { code: string; map: null } | null;
|
|
14
|
+
watchChange: (id: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const TEMPLATE_SEARCH = NAMESPACE + '.template(';
|
|
19
|
+
|
|
20
|
+
const TEMPLATE_CALL_REGEX = new RegExp(
|
|
21
|
+
'(const\\s+(\\w+)\\s*=\\s*' + NAMESPACE + '\\.template\\()(`)',
|
|
22
|
+
'g'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
let base = plugin.vite({
|
|
27
|
+
name: PACKAGE_NAME,
|
|
28
|
+
plugins: [reactivity, template]
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function injectHMR(code: string, id: string): string {
|
|
33
|
+
let hmrId = id.replace(/\\/g, '/'),
|
|
34
|
+
hotReplace = NAMESPACE + '.createHotTemplate("' + hmrId + '", "',
|
|
35
|
+
injected = code.replace(TEMPLATE_CALL_REGEX, function(_match: string, prefix: string, varName: string, backtick: string) {
|
|
36
|
+
return prefix.replace(TEMPLATE_SEARCH, hotReplace + varName + '", ') + backtick;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (injected === code) {
|
|
40
|
+
return code;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
injected += '\nif (import.meta.hot) { import.meta.hot.accept(() => { ' + NAMESPACE + '.accept("' + hmrId + '"); }); }';
|
|
44
|
+
|
|
45
|
+
return injected;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export default ({ root }: { root?: string } = {}) => {
|
|
50
|
+
let isDev = false,
|
|
51
|
+
vitePlugin = base({ root });
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...vitePlugin,
|
|
55
|
+
configResolved(config: any) {
|
|
56
|
+
vitePlugin.configResolved(config);
|
|
57
|
+
isDev = config?.command === 'serve' || config?.mode === 'development';
|
|
58
|
+
},
|
|
59
|
+
handleHotUpdate(_ctx: { file: string; modules: any[] }) {
|
|
60
|
+
// Let Vite handle the default HMR flow
|
|
61
|
+
},
|
|
62
|
+
transform(code: string, id: string) {
|
|
63
|
+
let result = vitePlugin.transform(code, id);
|
|
64
|
+
|
|
65
|
+
if (!result || !isDev) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let injected = injectHMR(result.code, id);
|
|
70
|
+
|
|
71
|
+
if (injected === result.code) {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { code: injected, map: null };
|
|
76
|
+
}
|
|
77
|
+
} satisfies VitePlugin;
|
|
78
|
+
};
|
|
79
|
+
|
package/src/hmr.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
let clone = <T extends DocumentFragment | Node>(node: T, deep: boolean = true) => node.cloneNode(deep) as T,
|
|
2
|
+
modules = new Map<string, Map<string, HotTemplate>>(),
|
|
3
|
+
tmpl = typeof document !== 'undefined' ? document.createElement('template') : null;
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type HotTemplate = {
|
|
7
|
+
cached: DocumentFragment | undefined;
|
|
8
|
+
factory: () => DocumentFragment;
|
|
9
|
+
html: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function invalidate(moduleId: string): void {
|
|
14
|
+
let templates = modules.get(moduleId);
|
|
15
|
+
|
|
16
|
+
if (!templates) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (let [, entry] of templates) {
|
|
21
|
+
entry.cached = undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function register(moduleId: string, templateId: string, html: string): () => DocumentFragment {
|
|
26
|
+
let entry: HotTemplate = {
|
|
27
|
+
cached: undefined,
|
|
28
|
+
factory: () => {
|
|
29
|
+
if (!entry.cached) {
|
|
30
|
+
let element = tmpl!.cloneNode() as HTMLTemplateElement;
|
|
31
|
+
|
|
32
|
+
element.innerHTML = entry.html;
|
|
33
|
+
entry.cached = element.content;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return clone(entry.cached!, true) as DocumentFragment;
|
|
37
|
+
},
|
|
38
|
+
html
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
(modules.get(moduleId) ?? (modules.set(moduleId, new Map()), modules.get(moduleId)!)).set(templateId, entry);
|
|
42
|
+
|
|
43
|
+
return entry.factory;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const accept = (moduleId: string): void => {
|
|
48
|
+
invalidate(moduleId);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const createHotTemplate = (moduleId: string, templateId: string, html: string): () => DocumentFragment => {
|
|
52
|
+
let existing = modules.get(moduleId)?.get(templateId);
|
|
53
|
+
|
|
54
|
+
if (existing) {
|
|
55
|
+
existing.cached = undefined;
|
|
56
|
+
existing.html = html;
|
|
57
|
+
|
|
58
|
+
return existing.factory;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return register(moduleId, templateId, html);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Test-only: reset state
|
|
65
|
+
const hmrReset = (): void => {
|
|
66
|
+
modules.clear();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export { accept, createHotTemplate, hmrReset, modules };
|