micra.js 2.0.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/dist/types.d.ts CHANGED
@@ -23,14 +23,21 @@ export interface FetchOptions {
23
23
  [key: string]: unknown;
24
24
  }
25
25
  /**
26
- * The `this` context inside component methods and lifecycle hooks.
27
- * `S` is inferred from the component's `state` object.
26
+ * User-defined methods on a component definition. Any function-shaped property
27
+ * other than `state`, `onCreate`, `onDestroy` is treated as a method.
28
28
  *
29
- * @example
30
- * // state: { count: 0 } S = { count: number }
31
- * increment() { this.state.count++ } // count is number
29
+ * LLM NOTE: this type is used as a structural HINT only — `M` in
30
+ * ComponentDefinition is unconstrained so TS can infer it from the literal
31
+ * without rejecting non-function siblings like `state`. The `[key: string]`
32
+ * shape here just documents intent.
33
+ */
34
+ export type ComponentMethods = Record<string, (...args: any[]) => any>;
35
+ /**
36
+ * Built-in slots every instance gets: state, refs, $el, and the methods Micra
37
+ * itself injects (render, destroy, prop, fetch, emit, on). Kept separate from
38
+ * `M` so user methods can't accidentally shadow these names.
32
39
  */
33
- export interface ComponentInstance<S extends StateRecord = StateRecord> {
40
+ export interface ComponentBuiltins<S extends StateRecord = StateRecord> {
34
41
  /** The root DOM element this component is mounted on. */
35
42
  readonly $el: HTMLElement;
36
43
  /** Reactive state — any assignment triggers a batched re-render. */
@@ -58,11 +65,36 @@ export interface ComponentInstance<S extends StateRecord = StateRecord> {
58
65
  /** Subscribe to the global bus. Subscription is auto-removed on destroy(). */
59
66
  on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn;
60
67
  }
68
+ /**
69
+ * The `this` context inside component methods and lifecycle hooks.
70
+ * `S` is inferred from the component's `state` object; `M` is inferred from
71
+ * the methods on the same definition object. Both `this.state.X` and
72
+ * `this.someMethod()` are fully typed inside method bodies.
73
+ *
74
+ * @example
75
+ * Micra.define('counter', {
76
+ * state: { count: 0 },
77
+ * inc() {
78
+ * this.state.count++ // this.state.count: number ✓
79
+ * this.dec() // this.dec: () => void ✓
80
+ * // this.foo() // ❌ Property 'foo' does not exist
81
+ * },
82
+ * dec() { this.state.count-- },
83
+ * })
84
+ */
85
+ export type ComponentInstance<S extends StateRecord = StateRecord, M = ComponentMethods> = ComponentBuiltins<S> & M;
61
86
  /**
62
87
  * Component definition passed to `Micra.define` or `Micra.mount`.
63
88
  *
64
- * `S` is inferred from the `state` property all methods receive
65
- * `this: ComponentInstance<S>` automatically via `ThisType<>`.
89
+ * Both `S` (state shape) and `M` (method set) are inferred from the literal.
90
+ * All methods and lifecycle hooks receive `this: ComponentInstance<S, M>` via
91
+ * `ThisType<>` — so `this.state.X` and `this.someMethod()` are both typed.
92
+ *
93
+ * LLM NOTE: `M` is intentionally unconstrained here. A constraint like
94
+ * `M extends ComponentMethods` would force every property in the literal to
95
+ * be a function — which would reject `state`. Without the constraint, TS
96
+ * structurally infers `M` as "everything in the literal except the known
97
+ * lifecycle/state keys", which is exactly what we want.
66
98
  *
67
99
  * @example
68
100
  * Micra.define('counter', {
@@ -70,21 +102,20 @@ export interface ComponentInstance<S extends StateRecord = StateRecord> {
70
102
  * inc() { this.state.count++ }, // this.state.count: number ✓
71
103
  * })
72
104
  */
73
- export type ComponentDefinition<S extends StateRecord = StateRecord> = {
105
+ export type ComponentDefinition<S extends StateRecord = StateRecord, M = ComponentMethods> = {
74
106
  /** Initial flat state. Becomes reactive on mount. */
75
107
  state?: S;
76
108
  /**
77
109
  * Called once after mount in a microtask — safe for async data fetching.
78
110
  * @example async onCreate() { this.state.data = await this.fetch('/api/data') }
79
111
  */
80
- onCreate?: () => void | Promise<void>;
112
+ onCreate?(): void | Promise<void>;
81
113
  /**
82
114
  * Called on destroy — clean up DOM listeners, timers, etc.
83
115
  * Event bus subscriptions added via `this.on()` are cleaned up automatically.
84
116
  */
85
- onDestroy?: () => void;
86
- [method: string]: unknown;
87
- } & ThisType<ComponentInstance<S>>;
117
+ onDestroy?(): void;
118
+ } & M & ThisType<ComponentInstance<S, M>>;
88
119
  /**
89
120
  * @internal Extended HTMLElement with Micra bookkeeping slots.
90
121
  */
@@ -94,7 +125,7 @@ export interface MicraElement extends HTMLElement {
94
125
  __micraAtBound?: true;
95
126
  __micraKey?: unknown;
96
127
  __micraEach?: true;
97
- __micraCache?: DirectiveCache;
128
+ __micraScan?: ScanIndex;
98
129
  }
99
130
  /**
100
131
  * @internal A DOM listener tracked for cleanup on destroy().
@@ -139,13 +170,15 @@ export interface CachedPairBinding {
139
170
  pairs: ReadonlyArray<readonly [string, string]>;
140
171
  }
141
172
  /**
142
- * @internal Directive scan result — built once per Element, reused every render.
143
- * This is the core of the performance optimization.
173
+ * @internal Single-pass scan result — built once per Element via one TreeWalker
174
+ * traversal, reused every render. This is the core of the performance
175
+ * optimization: instead of 10+ querySelectorAll calls per render, the scanner
176
+ * classifies every directive/event/ref attribute in a single DOM walk.
144
177
  *
145
- * LLM NOTE: DirectiveCache is built lazily on first render and stored on the
146
- * element. It avoids repeated querySelectorAll calls on every re-render.
178
+ * LLM NOTE: ScanIndex is built lazily on first render and stored on
179
+ * `el.__micraScan`. Subsequent renders skip the scan entirely.
147
180
  */
148
- export interface DirectiveCache {
181
+ export interface ScanIndex {
149
182
  text: CachedBinding[];
150
183
  html: CachedBinding[];
151
184
  if: CachedIfBinding[];
@@ -153,13 +186,22 @@ export interface DirectiveCache {
153
186
  bind: CachedPairBinding[];
154
187
  model: CachedBinding[];
155
188
  class: CachedPairBinding[];
189
+ each: Element[];
190
+ on: Element[];
191
+ atEvents: Element[];
192
+ refs: Element[];
156
193
  }
157
194
  /**
158
195
  * @internal Full instance as seen inside the runtime — extends the public
159
- * interface with private bookkeeping slots and an index signature for
196
+ * built-ins with private bookkeeping slots and an index signature for
160
197
  * dynamic method dispatch.
198
+ *
199
+ * Note: internally we don't carry the user-method generic `M`. Internal modules
200
+ * dispatch methods by string name (`instance[methodName]()`), which is what
201
+ * the index signature is for. The public `mount()` return value re-projects
202
+ * to `ComponentInstance<S, M>` so callers get full type inference.
161
203
  */
162
- export interface InternalInstance<S extends StateRecord = StateRecord> extends ComponentInstance<S> {
204
+ export interface InternalInstance<S extends StateRecord = StateRecord> extends ComponentBuiltins<S> {
163
205
  __micraSubs?: UnsubFn[];
164
206
  __micraListeners?: TrackedListener[];
165
207
  __micraDestroyed?: true;