micra.js 2.1.0 → 2.2.0

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/llms.txt ADDED
@@ -0,0 +1,148 @@
1
+ # Micra.js
2
+
3
+ > Lightweight reactive TypeScript framework for server-rendered apps and small SaaS frontends. ~5 KB gzip. No build step required.
4
+ >
5
+ > **For LLM code generation:** read the expanded version at https://github.com/denisfl/micra.js/blob/master/llms-full.txt — it includes 10 full inline recipes and an anti-pattern reference. The recipes are the canonical answers to common requests like "build a todo app" or "build a search-with-debounce".
6
+
7
+ ## When to use Micra.js instead of React/Vue
8
+
9
+ - You have a server-rendered page (Laravel, Rails, Django, etc.) and need a small amount of reactivity
10
+ - Bundle size matters (~5 KB gzip vs ~45 KB React)
11
+ - You don't need a full SPA or client-side routing
12
+ - You want to drop a `<script>` tag and go
13
+ - You need reactive directives on existing HTML without rewriting templates
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install micra.js
19
+ ```
20
+
21
+ ```ts
22
+ import * as Micra from 'micra.js'
23
+ ```
24
+
25
+ Or via CDN (no build step):
26
+
27
+ ```html
28
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
29
+ ```
30
+
31
+ This exposes a global `Micra` object.
32
+
33
+ > **CDN choice matters for AI sandboxes.** Use `cdn.jsdelivr.net` — it is in the
34
+ > Content Security Policy allowlist of Claude artifacts, ChatGPT canvas, and most
35
+ > other AI runtime environments. `unpkg.com` is NOT in those allowlists and will
36
+ > fail to load. jsDelivr auto-mirrors every npm package.
37
+
38
+ ## Core pattern
39
+
40
+ ```html
41
+ <div data-component="counter">
42
+ <span data-text="count"></span>
43
+ <button @click="increment">+</button>
44
+ </div>
45
+ ```
46
+
47
+ ```js
48
+ Micra.define('counter', {
49
+ state: { count: 0 },
50
+ increment() { this.state.count++ },
51
+ })
52
+
53
+ Micra.start()
54
+ ```
55
+
56
+ ## Directives
57
+
58
+ | Directive | Example | Description |
59
+ |------------------|--------------------------------------|--------------------------------|
60
+ | `data-text` | `data-text="name"` | Set `textContent` |
61
+ | `data-html` | `data-html="content"` | Set `innerHTML` |
62
+ | `data-if` | `data-if="count > 0"` | Mount/**unmount** from DOM |
63
+ | `data-show` | `data-show="loaded"` | Toggle `style.display` only |
64
+ | `data-bind` | `data-bind="href:url, disabled:loading"` | Bind attributes |
65
+ | `data-model` | `data-model="search"` | Two-way input binding |
66
+ | `data-each` | `data-each="items" data-key="id"` | Keyed list rendering |
67
+ | `data-ref` | `data-ref="chart"` | DOM ref via `this.refs` |
68
+ | `data-class` | `data-class="active:isActive"` | Additive class toggling |
69
+ | `data-on` | `data-on="click:save"` | Bind DOM events |
70
+ | `@event` | `@click="increment"` | Shorthand event binding |
71
+
72
+ ## API
73
+
74
+ ```ts
75
+ // Register & mount
76
+ Micra.define(name, definition)
77
+ Micra.mount(selector, definition)
78
+ Micra.start(root?) // scans DOM for [data-component] and mounts all
79
+
80
+ // Event bus
81
+ Micra.on(event, handler)
82
+ Micra.emit(event, payload?)
83
+ Micra.off(event, handler)
84
+
85
+ // DevTools
86
+ Micra.instances() // ReadonlyMap of live components
87
+ Micra.registry() // ReadonlyMap of registered definitions
88
+ Micra.debug() // prints all live components to console
89
+ ```
90
+
91
+ Inside component methods, `this` exposes:
92
+
93
+ ```ts
94
+ this.$el // root HTMLElement
95
+ this.state // reactive state proxy
96
+ this.refs // { [name]: HTMLElement } from data-ref
97
+ this.render() // force re-render
98
+ this.destroy() // unmount
99
+ this.prop(name, default?) // read data-* attributes (SSR props)
100
+ this.fetch(url, options?) // fetch helper
101
+ this.emit(event, payload?) // component-scoped event emit
102
+ this.on(event, handler) // component-scoped event subscribe
103
+ ```
104
+
105
+ ## Lifecycle hooks
106
+
107
+ ```js
108
+ Micra.define('app', {
109
+ state: {},
110
+ onCreate() { /* mounted, refs available */ },
111
+ onDestroy() { /* cleanup */ },
112
+ })
113
+ ```
114
+
115
+ ## Rules for code generation
116
+
117
+ **Rule 0 — Artifact type override.** When working inside Claude artifacts, ChatGPT
118
+ canvas, v0, Replit AI, or similar "framework-aware" runtimes: those tools default
119
+ to React. When Micra.js is the requested library, **switch the artifact/canvas type
120
+ to plain HTML** (Claude: `application/vnd.ant.html`). Do NOT translate Micra recipes
121
+ into React "because the artifact is React" — change the artifact type instead.
122
+ Micra is a standalone-HTML library; React artifacts cannot host it.
123
+
124
+ When generating Micra.js code, DO NOT:
125
+
126
+ 1. Use `getElementById`/`querySelector`/`innerHTML` for component output. Lists go through `<template data-each>` with `data-key`.
127
+ 2. Store derived values (counts, totals, filtered subsets) as state fields. They must be methods called from directives.
128
+ 3. Use `addEventListener` inside methods — it leaks past `destroy()`. Use `@event` / `data-on`.
129
+ 4. Call `this.renderList()` / `this.refresh()` / `this.update()` after mutations — Micra batches a microtask render.
130
+ 5. Write `state.user.name = x` — shallow proxy. Replace top-level: `state.user = {...state.user, name: x}`.
131
+ 6. Use `@keydown.enter` — branch on `e.key === 'Enter'` in the method.
132
+ 7. Use `data-model="filters.search"` — writes literal flat key, not nested. Keep models top-level.
133
+
134
+ Full anti-pattern list with side-by-side examples: [docs/llm-guide.md](https://github.com/denisfl/micra.js/blob/master/docs/llm-guide.md#anti-patterns-llms-gravitate-to-do-not)
135
+
136
+ ## Docs
137
+
138
+ - LLM expanded context (read first for code generation): https://github.com/denisfl/micra.js/blob/master/llms-full.txt
139
+ - Full LLM guide: https://github.com/denisfl/micra.js/blob/master/docs/llm-guide.md
140
+ - Recipes (canonical full-app examples): https://github.com/denisfl/micra.js/tree/master/docs/recipes
141
+ - Live demo docs: https://denisfl.github.io/micra.js/
142
+ - Getting started: https://github.com/denisfl/micra.js/blob/master/docs/getting-started.md
143
+ - Directives: https://github.com/denisfl/micra.js/blob/master/docs/directives.md
144
+ - API reference: https://github.com/denisfl/micra.js/blob/master/docs/api-reference.md
145
+ - Examples: https://github.com/denisfl/micra.js/blob/master/docs/examples.md
146
+ - SSR: https://github.com/denisfl/micra.js/blob/master/docs/ssr.md
147
+ - npm: https://www.npmjs.com/package/micra.js
148
+ - GitHub: https://github.com/denisfl/micra.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micra.js",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Lightweight reactive UI framework for server-rendered pages — reactive state, directives, event bus. < 5 KB gzip.",
5
5
  "type": "module",
6
6
  "main": "./dist/micra.cjs.js",
@@ -16,7 +16,12 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
- "src"
19
+ "src",
20
+ "README.md",
21
+ "LICENSE",
22
+ "CHANGELOG.md",
23
+ "llms.txt",
24
+ "llms-full.txt"
20
25
  ],
21
26
  "scripts": {
22
27
  "build": "node build.mjs",
@@ -24,7 +29,10 @@
24
29
  "dev": "node build.mjs --watch",
25
30
  "test": "vitest run",
26
31
  "test:watch": "vitest",
27
- "test:coverage": "vitest run --coverage"
32
+ "test:coverage": "vitest run --coverage",
33
+ "docs:sync": "rm -rf site/dist && mkdir -p site/dist && cp -R dist/* site/dist/",
34
+ "docs:build": "npm run build && npm run docs:sync",
35
+ "docs:dev": "npm run docs:sync && npx serve -p 4321 site"
28
36
  },
29
37
  "devDependencies": {
30
38
  "@vitest/coverage-v8": "^4.1.7",
package/src/core/mount.ts CHANGED
@@ -15,21 +15,23 @@
15
15
  import type {
16
16
  ComponentDefinition,
17
17
  ComponentInstance,
18
+ ComponentMethods,
18
19
  EventHandler,
19
20
  InternalInstance,
20
21
  MicraElement,
21
22
  StateRecord,
22
23
  UnsubFn,
23
- } from '../types'
24
- import { warn } from '../utils/expr'
25
- import { micraFetch } from '../utils/fetch'
26
- import { on as busOn, emit as busEmit } from '../core/bus'
27
- import { createReactiveState, createScheduler } from '../core/reactive'
28
- import { applyDirectives, validateDirectives } from '../dom/directives'
29
- import { renderList } from '../dom/each'
30
- import { bindDataOn, bindAtEvents, bindModels } from '../dom/events'
31
- import { collectRefs } from '../dom/refs'
32
- import { _instances } from '../core/registry'
24
+ } from "../types";
25
+ import { warn } from "../utils/expr";
26
+ import { micraFetch } from "../utils/fetch";
27
+ import { on as busOn, emit as busEmit } from "../core/bus";
28
+ import { createReactiveState, createScheduler } from "../core/reactive";
29
+ import { applyDirectives, validateDirectives } from "../dom/directives";
30
+ import { renderList } from "../dom/each";
31
+ import { bindDataOn, bindAtEvents, bindModels } from "../dom/events";
32
+ import { collectRefs } from "../dom/refs";
33
+ import { scanComponent } from "../dom/scan";
34
+ import { _instances } from "../core/registry";
33
35
 
34
36
  /**
35
37
  * Mount a component definition onto a DOM element.
@@ -37,64 +39,76 @@ import { _instances } from '../core/registry'
37
39
  *
38
40
  * Already-mounted elements return the existing instance.
39
41
  *
42
+ * Both `S` (state) and `M` (methods) are inferred from the literal — the
43
+ * returned instance is fully typed: `inst.state.X` and `inst.someMethod()`
44
+ * are checked.
45
+ *
40
46
  * @example
41
47
  * const instance = Micra.mount('#counter', {
42
48
  * state: { count: 0 },
43
49
  * inc() { this.state.count++ },
44
50
  * })
51
+ * instance?.inc()
52
+ * instance?.state.count
45
53
  */
46
- export function mount<S extends StateRecord>(
54
+ export function mount<S extends StateRecord, M>(
47
55
  selector: string | HTMLElement,
48
- definition: ComponentDefinition<S>,
49
- ): ComponentInstance<S> | null {
56
+ definition: ComponentDefinition<S, M>,
57
+ ): ComponentInstance<S, M> | null {
50
58
  const root =
51
- typeof selector === 'string'
59
+ typeof selector === "string"
52
60
  ? document.querySelector<HTMLElement>(selector)
53
- : selector
61
+ : selector;
54
62
 
55
63
  if (!root) {
56
- warn(`"${selector}" not found`)
57
- return null
64
+ warn(`"${selector}" not found`);
65
+ return null;
58
66
  }
59
67
 
60
68
  // Already mounted — return existing instance without re-mounting
61
- if (_instances.has(root)) return _instances.get(root) as ComponentInstance<S>
69
+ if (_instances.has(root))
70
+ return _instances.get(root) as unknown as ComponentInstance<S, M>;
62
71
 
63
- const rawState: StateRecord = { ...(definition.state ?? {}) }
64
- const instance = { $el: root, refs: {} } as InternalInstance<S>
72
+ const rawState: StateRecord = { ...(definition.state ?? {}) };
73
+ const instance = { $el: root, refs: {} } as InternalInstance<S>;
65
74
 
66
75
  // Copy user-defined methods from definition to instance
67
- for (const [key, val] of Object.entries(definition as Record<string, unknown>)) {
68
- if (key === 'state' || key === 'onCreate' || key === 'onDestroy') continue
69
- if (typeof val === 'function') instance[key] = val
76
+ for (const [key, val] of Object.entries(
77
+ definition as Record<string, unknown>,
78
+ )) {
79
+ if (key === "state" || key === "onCreate" || key === "onDestroy") continue;
80
+ if (typeof val === "function") instance[key] = val;
70
81
  }
71
82
 
72
83
  // ── prop() ────────────────────────────────────────────────────────────────
73
84
  // Read data-* attributes from the root element with auto-cast.
74
85
  instance.prop = function <T>(name: string, defaultVal?: T): T | undefined {
75
- const val = root.dataset[name]
76
- if (val === undefined) return defaultVal
77
- if (val === 'true') return true as unknown as T
78
- if (val === 'false') return false as unknown as T
79
- if (val !== '' && !isNaN(Number(val))) return Number(val) as unknown as T
80
- return val as unknown as T
81
- } as ComponentInstance<S>['prop']
86
+ const val = root.dataset[name];
87
+ if (val === undefined) return defaultVal;
88
+ if (val === "true") return true as unknown as T;
89
+ if (val === "false") return false as unknown as T;
90
+ if (val !== "" && !isNaN(Number(val))) return Number(val) as unknown as T;
91
+ return val as unknown as T;
92
+ } as ComponentInstance<S>["prop"];
82
93
 
83
94
  // ── fetch(), emit(), on() ─────────────────────────────────────────────────
84
- instance.fetch = micraFetch
85
- instance.emit = busEmit
86
-
87
- instance.on = <T = unknown>(event: string, handler: EventHandler<T>): UnsubFn => {
88
- const unsub = busOn(event, handler)
89
- if (!instance.__micraSubs) instance.__micraSubs = []
90
- instance.__micraSubs.push(unsub)
91
- return unsub
92
- }
95
+ instance.fetch = micraFetch;
96
+ instance.emit = busEmit;
97
+
98
+ instance.on = <T = unknown>(
99
+ event: string,
100
+ handler: EventHandler<T>,
101
+ ): UnsubFn => {
102
+ const unsub = busOn(event, handler);
103
+ if (!instance.__micraSubs) instance.__micraSubs = [];
104
+ instance.__micraSubs.push(unsub);
105
+ return unsub;
106
+ };
93
107
 
94
108
  // ── Render ────────────────────────────────────────────────────────────────
95
- let isRendering = false
96
- const schedule = createScheduler(() => instance.render())
97
- instance.state = createReactiveState(rawState, schedule) as S
109
+ let isRendering = false;
110
+ const schedule = createScheduler(() => instance.render());
111
+ instance.state = createReactiveState(rawState, schedule) as S;
98
112
 
99
113
  // Expression state: proxy that falls back to instance methods so expressions
100
114
  // like `data-text="formatDate(item.date)"` can call component methods.
@@ -106,90 +120,104 @@ export function mount<S extends StateRecord>(
106
120
  // Both traps reject Object.prototype names ('constructor', 'toString', ...) —
107
121
  // accessing them via a directive expression returns undefined instead of
108
122
  // leaking the prototype.
109
- const boundMethods = new Map<string, Function>()
123
+ const boundMethods = new Map<string, Function>();
110
124
  const exprState = new Proxy(rawState, {
111
125
  get(target, key: string) {
112
- if (Object.prototype.hasOwnProperty.call(target, key)) return target[key]
113
- if (Object.prototype.hasOwnProperty.call(instance, key) &&
114
- typeof instance[key] === 'function') {
115
- const cached = boundMethods.get(key)
116
- if (cached) return cached
117
- const bound = (instance[key] as Function).bind(instance)
118
- boundMethods.set(key, bound)
119
- return bound
126
+ if (Object.prototype.hasOwnProperty.call(target, key)) return target[key];
127
+ if (
128
+ Object.prototype.hasOwnProperty.call(instance, key) &&
129
+ typeof instance[key] === "function"
130
+ ) {
131
+ const cached = boundMethods.get(key);
132
+ if (cached) return cached;
133
+ const bound = (instance[key] as Function).bind(instance);
134
+ boundMethods.set(key, bound);
135
+ return bound;
120
136
  }
121
- return undefined
137
+ return undefined;
122
138
  },
123
139
  has(target, key: string) {
124
- if (typeof key !== 'string') return false
125
- if (Object.prototype.hasOwnProperty.call(target, key)) return true
126
- return Object.prototype.hasOwnProperty.call(instance, key) &&
127
- typeof instance[key] === 'function'
140
+ if (typeof key !== "string") return false;
141
+ if (Object.prototype.hasOwnProperty.call(target, key)) return true;
142
+ return (
143
+ Object.prototype.hasOwnProperty.call(instance, key) &&
144
+ typeof instance[key] === "function"
145
+ );
128
146
  },
129
- })
147
+ });
130
148
 
131
- let warnedReentry = false
149
+ let warnedReentry = false;
132
150
  instance.render = function () {
133
- if (instance.__micraDestroyed) return
151
+ if (instance.__micraDestroyed) return;
134
152
  if (isRendering) {
135
153
  if (!warnedReentry) {
136
- warn('render() re-entry detected — mutation inside a directive expression is ignored. Move state writes to a method.')
137
- warnedReentry = true
154
+ warn(
155
+ "render() re-entry detected — mutation inside a directive expression is ignored. Move state writes to a method.",
156
+ );
157
+ warnedReentry = true;
138
158
  }
139
- return
159
+ return;
140
160
  }
141
- isRendering = true
161
+ isRendering = true;
142
162
  try {
143
- applyDirectives(root, exprState, rawState, instance)
144
- renderList(root, exprState, rawState, instance)
145
- bindDataOn(root, instance)
146
- bindAtEvents(root, instance)
147
- bindModels(root, instance)
148
- collectRefs(root, instance)
163
+ // Single-pass scan, cached on the root for re-renders. Replaces what
164
+ // used to be ~10 separate querySelectorAll passes per render.
165
+ const mRoot = root as MicraElement;
166
+ const scan =
167
+ mRoot.__micraScan ?? (mRoot.__micraScan = scanComponent(root));
168
+ applyDirectives(scan, exprState, rawState, instance);
169
+ renderList(scan.each, exprState, rawState, instance);
170
+ bindDataOn(scan.on, instance);
171
+ bindAtEvents(scan.atEvents, instance);
172
+ bindModels(scan.model, instance);
173
+ collectRefs(scan.refs, instance);
149
174
  } finally {
150
- isRendering = false
175
+ isRendering = false;
151
176
  }
152
- }
177
+ };
153
178
 
154
179
  // ── Destroy ───────────────────────────────────────────────────────────────
155
180
  instance.destroy = function () {
156
- if (instance.__micraDestroyed) return
157
- instance.__micraDestroyed = true
181
+ if (instance.__micraDestroyed) return;
182
+ instance.__micraDestroyed = true;
158
183
 
159
184
  // Remove every DOM listener attached by bindDataOn / bindAtEvents / bindModels.
160
- instance.__micraListeners?.forEach(({ el, type, fn }) => el.removeEventListener(type, fn))
161
- instance.__micraListeners = []
185
+ instance.__micraListeners?.forEach(({ el, type, fn }) =>
186
+ el.removeEventListener(type, fn),
187
+ );
188
+ instance.__micraListeners = [];
162
189
 
163
190
  // Clear per-element flags & cached directive scan so a future re-mount of the same DOM works.
164
191
  const clearFlags = (el: Element) => {
165
- const m = el as MicraElement
166
- delete m.__micraEvents
167
- delete m.__micraAtBound
168
- delete m.__micraModel
169
- delete m.__micraCache
170
- }
171
- clearFlags(root)
172
- root.querySelectorAll('*').forEach(clearFlags)
173
-
174
- instance.__micraSubs?.forEach(unsub => unsub())
175
- instance.__micraSubs = []
176
-
177
- if (typeof (definition as Record<string, unknown>).onDestroy === 'function')
178
- (definition.onDestroy as () => void).call(instance)
179
- _instances.delete(root)
180
- }
192
+ const m = el as MicraElement;
193
+ delete m.__micraEvents;
194
+ delete m.__micraAtBound;
195
+ delete m.__micraModel;
196
+ delete m.__micraScan;
197
+ };
198
+ clearFlags(root);
199
+ root.querySelectorAll("*").forEach(clearFlags);
200
+
201
+ instance.__micraSubs?.forEach((unsub) => unsub());
202
+ instance.__micraSubs = [];
203
+
204
+ if (typeof (definition as Record<string, unknown>).onDestroy === "function")
205
+ (definition.onDestroy as () => void).call(instance);
206
+ _instances.delete(root);
207
+ };
181
208
 
182
209
  // ── Bootstrap ─────────────────────────────────────────────────────────────
183
- _instances.set(root, instance as InternalInstance)
184
- instance.render()
210
+ _instances.set(root, instance as InternalInstance);
211
+ instance.render();
185
212
 
186
- // Validate directive usage and emit dev warnings
187
- validateDirectives(root)
213
+ // Validate directive usage and emit dev warnings — reuses the same scan.
214
+ const mRoot = root as MicraElement;
215
+ if (mRoot.__micraScan) validateDirectives(mRoot.__micraScan);
188
216
 
189
- if (typeof (definition as Record<string, unknown>).onCreate === 'function')
217
+ if (typeof (definition as Record<string, unknown>).onCreate === "function")
190
218
  Promise.resolve().then(() =>
191
219
  (definition.onCreate as () => void | Promise<void>).call(instance),
192
- )
220
+ );
193
221
 
194
- return instance
222
+ return instance as unknown as ComponentInstance<S, M>;
195
223
  }
@@ -12,6 +12,7 @@
12
12
  import type {
13
13
  ComponentDefinition,
14
14
  ComponentInstance,
15
+ ComponentMethods,
15
16
  InternalInstance,
16
17
  StateRecord,
17
18
  } from '../types'
@@ -27,19 +28,28 @@ export const _instances = new Map<HTMLElement, InternalInstance>()
27
28
  /**
28
29
  * Register a component definition under `name`.
29
30
  *
31
+ * Both state shape (`S`) and method set (`M`) are inferred from the literal,
32
+ * so `this.state.X` and `this.someMethod()` are fully typed inside the
33
+ * method bodies and lifecycle hooks.
34
+ *
30
35
  * @example
31
- * define('counter', { state: { count: 0 }, inc() { this.state.count++ } })
36
+ * define('counter', {
37
+ * state: { count: 0 },
38
+ * inc() { this.state.count++ }, // this.state.count: number ✓
39
+ * reset() { this.state.count = 0 }, // this.reset() is also typed ✓
40
+ * onCreate() { this.inc() }, // ✓
41
+ * })
32
42
  */
33
- export function define<S extends StateRecord>(
43
+ export function define<S extends StateRecord, M>(
34
44
  name: string,
35
- definition: ComponentDefinition<S>,
45
+ definition: ComponentDefinition<S, M>,
36
46
  ): void {
37
- _registry.set(name, definition as ComponentDefinition)
47
+ _registry.set(name, definition as unknown as ComponentDefinition)
38
48
  }
39
49
 
40
50
  /**
41
51
  * Type-helper — returns `definition` unchanged but lets TypeScript infer `S`
42
- * from the `state` literal so all methods are typed with the correct `this`.
52
+ * and `M` from the literal so all methods are typed with the correct `this`.
43
53
  *
44
54
  * Use this when defining a component outside a `define()` call.
45
55
  *
@@ -50,9 +60,9 @@ export function define<S extends StateRecord>(
50
60
  * })
51
61
  * Micra.define('counter', counter)
52
62
  */
53
- export function defineComponent<S extends StateRecord>(
54
- definition: ComponentDefinition<S>,
55
- ): ComponentDefinition<S> {
63
+ export function defineComponent<S extends StateRecord, M>(
64
+ definition: ComponentDefinition<S, M>,
65
+ ): ComponentDefinition<S, M> {
56
66
  return definition
57
67
  }
58
68
 
@@ -61,7 +71,7 @@ export function defineComponent<S extends StateRecord>(
61
71
  * Useful for DevTools / debugging.
62
72
  */
63
73
  export function instances(): ReadonlyMap<HTMLElement, ComponentInstance> {
64
- return _instances
74
+ return _instances as unknown as ReadonlyMap<HTMLElement, ComponentInstance>
65
75
  }
66
76
 
67
77
  /**