@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 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
- | `attributes` | Attribute manipulation utilities |
269
- | `event` | Event system |
270
- | `slot` | Slot rendering |
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 | Node | string | number | null | undefined;
278
- type Element = HTMLElement & { [STORE]?: Record<string, unknown> };
279
- type Attributes = Record<string, unknown>;
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: (config: unknown) => void;
5
- enforce: "pre";
6
- name: string;
7
- transform: (code: string, id: string) => {
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
- export default plugin.vite({
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
@@ -1,5 +1,6 @@
1
1
  export * from './attributes.js';
2
2
  export * from './event/index.js';
3
+ export * from './hmr.js';
3
4
  export * from './utilities.js';
4
5
  export { default as html } from './html.js';
5
6
  export { default as render } from './render.js';
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';
@@ -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
- parent.insertBefore(node, ref);
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) {
@@ -1,11 +1,11 @@
1
- import { Element, Renderable, SlotGroup } from '../types.js';
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: (dispose?: VoidFunction) => Renderable<any>);
8
+ constructor(anchor: Element, fn: ((...args: any[]) => any));
9
9
  dispose(): void;
10
10
  update(value: unknown): void;
11
11
  }
@@ -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 = effect(() => {
24
- value = read(fn(dispose));
25
- if (!this.disposer) {
26
- this.update(value);
27
- }
28
- else if (!this.scheduled) {
29
- this.scheduled = true;
30
- raf(() => {
31
- this.scheduled = false;
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
- this.disposer();
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 = fragment.firstChild;
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: fragment.lastChild
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` - Called when element enters DOM
273
- - `ondisconnect` - Called when element leaves DOM
274
- - `onrender` - Called after initial render
275
- - `onresize` - ResizeObserver-based
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 ← types, cleanup
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
  ```
package/package.json CHANGED
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "type": "module",
40
40
  "types": "./build/index.d.ts",
41
- "version": "0.16.15",
41
+ "version": "0.17.2",
42
42
  "scripts": {
43
43
  "bench:run": "vitest bench --config vitest.bench.config.ts",
44
44
  "build": "tsc",