micra.js 2.1.0 → 2.2.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/CHANGELOG.md +244 -0
- package/README.md +39 -14
- package/dist/core/mount.d.ts +8 -2
- package/dist/core/reactive.d.ts +1 -1
- package/dist/core/registry.d.ts +13 -4
- package/dist/dom/directives.d.ts +11 -15
- package/dist/dom/each.d.ts +10 -7
- package/dist/dom/events.d.ts +15 -5
- package/dist/dom/refs.d.ts +5 -5
- package/dist/dom/scan.d.ts +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +302 -177
- package/dist/micra.cjs.js.map +4 -4
- package/dist/micra.esm.js +302 -177
- package/dist/micra.esm.js.map +4 -4
- package/dist/micra.js +302 -177
- package/dist/micra.js.map +4 -4
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +67 -22
- package/llms-full.txt +600 -0
- package/llms.txt +148 -0
- package/package.json +11 -3
- package/src/core/mount.ts +136 -99
- package/src/core/reactive.ts +2 -1
- package/src/core/registry.ts +19 -9
- package/src/dom/directives.ts +39 -122
- package/src/dom/each.ts +133 -37
- package/src/dom/events.ts +23 -31
- package/src/dom/refs.ts +7 -7
- package/src/dom/scan.ts +189 -0
- package/src/index.ts +2 -0
- package/src/types.ts +80 -22
- package/src/utils/expr.ts +34 -21
package/src/dom/scan.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/dom/scan.ts — Single-pass directive/event/ref scanner.
|
|
3
|
+
*
|
|
4
|
+
* Replaces 10+ querySelectorAll calls per render with ONE TreeWalker
|
|
5
|
+
* traversal that classifies every directive attribute in a single visit.
|
|
6
|
+
*
|
|
7
|
+
* Boundaries:
|
|
8
|
+
* - REJECT (skip subtree) on nested [data-component] — same semantics as
|
|
9
|
+
* the old `filterOwn` helper, but applied during the walk so we don't
|
|
10
|
+
* even *visit* those nodes.
|
|
11
|
+
* - <template> contents are not visited (browser TreeWalker default).
|
|
12
|
+
* `<template data-each>` itself IS visited and classified into scan.each;
|
|
13
|
+
* its children are processed by each.ts on every render via scanFragment.
|
|
14
|
+
*
|
|
15
|
+
* Hot-path notes:
|
|
16
|
+
* - We read `el.attributes` once and switch by suffix. No allocations per
|
|
17
|
+
* non-matching attr.
|
|
18
|
+
* - Pair-parsing (`data-bind`, `data-class`) happens here, once, at scan
|
|
19
|
+
* time. Reused on every render.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { CachedIfBinding, CachedPairBinding, ScanIndex } from "../types";
|
|
23
|
+
|
|
24
|
+
function emptyScan(): ScanIndex {
|
|
25
|
+
return {
|
|
26
|
+
text: [],
|
|
27
|
+
html: [],
|
|
28
|
+
if: [],
|
|
29
|
+
show: [],
|
|
30
|
+
bind: [],
|
|
31
|
+
model: [],
|
|
32
|
+
class: [],
|
|
33
|
+
each: [],
|
|
34
|
+
on: [],
|
|
35
|
+
atEvents: [],
|
|
36
|
+
refs: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @internal Parse `name:expr, name2:expr2` once at scan time. */
|
|
41
|
+
function parsePairs(expr: string): Array<readonly [string, string]> {
|
|
42
|
+
const out: Array<readonly [string, string]> = [];
|
|
43
|
+
for (const part of expr.split(",")) {
|
|
44
|
+
const colon = part.indexOf(":");
|
|
45
|
+
if (colon === -1) continue;
|
|
46
|
+
const left = part.slice(0, colon).trim();
|
|
47
|
+
const right = part.slice(colon + 1).trim();
|
|
48
|
+
if (!left) continue;
|
|
49
|
+
out.push([left, right]);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @internal Classify every relevant attribute on one element. */
|
|
55
|
+
function classify(el: Element, scan: ScanIndex): void {
|
|
56
|
+
// <template data-each> is the only directive we permit on a <template>.
|
|
57
|
+
// Other directives on a <template> would be meaningless (the content lives
|
|
58
|
+
// in template.content, not template.children).
|
|
59
|
+
if (el.tagName === "TEMPLATE") {
|
|
60
|
+
if (el.hasAttribute("data-each")) scan.each.push(el);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const attrs = el.attributes;
|
|
65
|
+
let atEventSeen = false;
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
68
|
+
const a = attrs[i]!;
|
|
69
|
+
const name = a.name;
|
|
70
|
+
|
|
71
|
+
// Fast path: most attribute names aren't ours. First-char check rejects
|
|
72
|
+
// the common case (id, class, style, href, …) without a string compare.
|
|
73
|
+
const first = name.charCodeAt(0);
|
|
74
|
+
|
|
75
|
+
if (first === 64 /* '@' */) {
|
|
76
|
+
// @event="method" or @event.modifier="method"
|
|
77
|
+
if (!atEventSeen) {
|
|
78
|
+
scan.atEvents.push(el);
|
|
79
|
+
atEventSeen = true;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// data-X attributes
|
|
85
|
+
if (
|
|
86
|
+
first === 100 /* d */ &&
|
|
87
|
+
name.length >= 6 &&
|
|
88
|
+
name.charCodeAt(4) === 45 /* '-' */
|
|
89
|
+
) {
|
|
90
|
+
// 'data-' prefix
|
|
91
|
+
const rest = name.slice(5);
|
|
92
|
+
switch (rest) {
|
|
93
|
+
case "text":
|
|
94
|
+
scan.text.push({ el, expr: a.value });
|
|
95
|
+
break;
|
|
96
|
+
case "html":
|
|
97
|
+
scan.html.push({ el, expr: a.value });
|
|
98
|
+
break;
|
|
99
|
+
case "if":
|
|
100
|
+
scan.if.push({ el, expr: a.value } as CachedIfBinding);
|
|
101
|
+
break;
|
|
102
|
+
case "show":
|
|
103
|
+
scan.show.push({ el, expr: a.value });
|
|
104
|
+
break;
|
|
105
|
+
case "bind": {
|
|
106
|
+
const pairs = parsePairs(a.value);
|
|
107
|
+
scan.bind.push({ el, expr: a.value, pairs } as CachedPairBinding);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "model":
|
|
111
|
+
scan.model.push({ el, expr: a.value });
|
|
112
|
+
break;
|
|
113
|
+
case "class": {
|
|
114
|
+
const pairs = parsePairs(a.value);
|
|
115
|
+
scan.class.push({ el, expr: a.value, pairs } as CachedPairBinding);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "on":
|
|
119
|
+
scan.on.push(el);
|
|
120
|
+
break;
|
|
121
|
+
case "ref":
|
|
122
|
+
scan.refs.push(el);
|
|
123
|
+
break;
|
|
124
|
+
// data-key, data-each, data-component, data-* user attrs — ignored here
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @internal Shared filter: stop descending into nested components. */
|
|
131
|
+
const NESTED_COMPONENT_FILTER: NodeFilter = {
|
|
132
|
+
acceptNode(node: Node): number {
|
|
133
|
+
if ((node as Element).hasAttribute("data-component"))
|
|
134
|
+
return NodeFilter.FILTER_REJECT;
|
|
135
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Scan an Element subtree owned by one component. Skips nested
|
|
141
|
+
* [data-component] subtrees entirely. Visits the root itself.
|
|
142
|
+
*
|
|
143
|
+
* Cached on `el.__micraScan` after the first call — subsequent renders
|
|
144
|
+
* are free.
|
|
145
|
+
*/
|
|
146
|
+
export function scanComponent(root: Element): ScanIndex {
|
|
147
|
+
const scan = emptyScan();
|
|
148
|
+
|
|
149
|
+
// Always classify root itself first — the TreeWalker's filter would
|
|
150
|
+
// REJECT it if it had `data-component` (which it normally does for a
|
|
151
|
+
// mounted component). The filter is for *descendants*.
|
|
152
|
+
classify(root, scan);
|
|
153
|
+
|
|
154
|
+
const walker = document.createTreeWalker(
|
|
155
|
+
root,
|
|
156
|
+
NodeFilter.SHOW_ELEMENT,
|
|
157
|
+
NESTED_COMPONENT_FILTER,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
let node: Element | null = walker.nextNode() as Element | null;
|
|
161
|
+
while (node) {
|
|
162
|
+
classify(node, scan);
|
|
163
|
+
node = walker.nextNode() as Element | null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return scan;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Scan a DocumentFragment (no-key each clone). Not cached — these fragments
|
|
171
|
+
* are temporary and re-cloned every render.
|
|
172
|
+
*/
|
|
173
|
+
export function scanFragment(frag: DocumentFragment): ScanIndex {
|
|
174
|
+
const scan = emptyScan();
|
|
175
|
+
|
|
176
|
+
const walker = document.createTreeWalker(
|
|
177
|
+
frag,
|
|
178
|
+
NodeFilter.SHOW_ELEMENT,
|
|
179
|
+
NESTED_COMPONENT_FILTER,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
let node: Element | null = walker.nextNode() as Element | null;
|
|
183
|
+
while (node) {
|
|
184
|
+
classify(node, scan);
|
|
185
|
+
node = walker.nextNode() as Element | null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return scan;
|
|
189
|
+
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -32,14 +32,23 @@ export interface FetchOptions {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
* `
|
|
35
|
+
* User-defined methods on a component definition. Any function-shaped property
|
|
36
|
+
* other than `state`, `onCreate`, `onDestroy` is treated as a method.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* LLM NOTE: this type is used as a structural HINT only — `M` in
|
|
39
|
+
* ComponentDefinition is unconstrained so TS can infer it from the literal
|
|
40
|
+
* without rejecting non-function siblings like `state`. The `[key: string]`
|
|
41
|
+
* shape here just documents intent.
|
|
42
|
+
*/
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
export type ComponentMethods = Record<string, (...args: any[]) => any>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Built-in slots every instance gets: state, refs, $el, and the methods Micra
|
|
48
|
+
* itself injects (render, destroy, prop, fetch, emit, on). Kept separate from
|
|
49
|
+
* `M` so user methods can't accidentally shadow these names.
|
|
41
50
|
*/
|
|
42
|
-
export interface
|
|
51
|
+
export interface ComponentBuiltins<S extends StateRecord = StateRecord> {
|
|
43
52
|
/** The root DOM element this component is mounted on. */
|
|
44
53
|
readonly $el: HTMLElement
|
|
45
54
|
/** Reactive state — any assignment triggers a batched re-render. */
|
|
@@ -68,11 +77,40 @@ export interface ComponentInstance<S extends StateRecord = StateRecord> {
|
|
|
68
77
|
on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The `this` context inside component methods and lifecycle hooks.
|
|
82
|
+
* `S` is inferred from the component's `state` object; `M` is inferred from
|
|
83
|
+
* the methods on the same definition object. Both `this.state.X` and
|
|
84
|
+
* `this.someMethod()` are fully typed inside method bodies.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* Micra.define('counter', {
|
|
88
|
+
* state: { count: 0 },
|
|
89
|
+
* inc() {
|
|
90
|
+
* this.state.count++ // this.state.count: number ✓
|
|
91
|
+
* this.dec() // this.dec: () => void ✓
|
|
92
|
+
* // this.foo() // ❌ Property 'foo' does not exist
|
|
93
|
+
* },
|
|
94
|
+
* dec() { this.state.count-- },
|
|
95
|
+
* })
|
|
96
|
+
*/
|
|
97
|
+
export type ComponentInstance<
|
|
98
|
+
S extends StateRecord = StateRecord,
|
|
99
|
+
M = ComponentMethods,
|
|
100
|
+
> = ComponentBuiltins<S> & M
|
|
101
|
+
|
|
71
102
|
/**
|
|
72
103
|
* Component definition passed to `Micra.define` or `Micra.mount`.
|
|
73
104
|
*
|
|
74
|
-
* `S`
|
|
75
|
-
* `this: ComponentInstance<S>`
|
|
105
|
+
* Both `S` (state shape) and `M` (method set) are inferred from the literal.
|
|
106
|
+
* All methods and lifecycle hooks receive `this: ComponentInstance<S, M>` via
|
|
107
|
+
* `ThisType<>` — so `this.state.X` and `this.someMethod()` are both typed.
|
|
108
|
+
*
|
|
109
|
+
* LLM NOTE: `M` is intentionally unconstrained here. A constraint like
|
|
110
|
+
* `M extends ComponentMethods` would force every property in the literal to
|
|
111
|
+
* be a function — which would reject `state`. Without the constraint, TS
|
|
112
|
+
* structurally infers `M` as "everything in the literal except the known
|
|
113
|
+
* lifecycle/state keys", which is exactly what we want.
|
|
76
114
|
*
|
|
77
115
|
* @example
|
|
78
116
|
* Micra.define('counter', {
|
|
@@ -80,21 +118,23 @@ export interface ComponentInstance<S extends StateRecord = StateRecord> {
|
|
|
80
118
|
* inc() { this.state.count++ }, // this.state.count: number ✓
|
|
81
119
|
* })
|
|
82
120
|
*/
|
|
83
|
-
export type ComponentDefinition<
|
|
121
|
+
export type ComponentDefinition<
|
|
122
|
+
S extends StateRecord = StateRecord,
|
|
123
|
+
M = ComponentMethods,
|
|
124
|
+
> = {
|
|
84
125
|
/** Initial flat state. Becomes reactive on mount. */
|
|
85
126
|
state?: S
|
|
86
127
|
/**
|
|
87
128
|
* Called once after mount in a microtask — safe for async data fetching.
|
|
88
129
|
* @example async onCreate() { this.state.data = await this.fetch('/api/data') }
|
|
89
130
|
*/
|
|
90
|
-
onCreate
|
|
131
|
+
onCreate?(): void | Promise<void>
|
|
91
132
|
/**
|
|
92
133
|
* Called on destroy — clean up DOM listeners, timers, etc.
|
|
93
134
|
* Event bus subscriptions added via `this.on()` are cleaned up automatically.
|
|
94
135
|
*/
|
|
95
|
-
onDestroy
|
|
96
|
-
|
|
97
|
-
} & ThisType<ComponentInstance<S>>
|
|
136
|
+
onDestroy?(): void
|
|
137
|
+
} & M & ThisType<ComponentInstance<S, M>>
|
|
98
138
|
|
|
99
139
|
// ── Internal types ────────────────────────────────────────────────────────────
|
|
100
140
|
// These are NOT exported from src/index.ts.
|
|
@@ -108,7 +148,10 @@ export interface MicraElement extends HTMLElement {
|
|
|
108
148
|
__micraAtBound?: true // @event shorthand bound (per-element)
|
|
109
149
|
__micraKey?: unknown // keyed-diff key
|
|
110
150
|
__micraEach?: true // belongs to a no-key each list
|
|
111
|
-
|
|
151
|
+
__micraScan?: ScanIndex // single-pass scan result (cached after 1st render)
|
|
152
|
+
__micraItem?: StateRecord // keyed row: last-rendered item ref (for skip check)
|
|
153
|
+
__micraIndex?: number // keyed row: last-rendered index (for skip check)
|
|
154
|
+
_itemState?: StateRecord // keyed row: reused itemState (avoids Object.create per render)
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
/**
|
|
@@ -126,7 +169,7 @@ export interface TrackedListener {
|
|
|
126
169
|
export interface MicraTemplate extends HTMLTemplateElement {
|
|
127
170
|
__micraMarker?: Comment
|
|
128
171
|
__micraNodes: Map<unknown, MicraElement>
|
|
129
|
-
__micraList:
|
|
172
|
+
__micraList: MicraElement[]
|
|
130
173
|
__micraNoKeyWarned?: true
|
|
131
174
|
}
|
|
132
175
|
|
|
@@ -159,13 +202,16 @@ export interface CachedPairBinding {
|
|
|
159
202
|
}
|
|
160
203
|
|
|
161
204
|
/**
|
|
162
|
-
* @internal
|
|
163
|
-
* This is the core of the performance
|
|
205
|
+
* @internal Single-pass scan result — built once per Element via one TreeWalker
|
|
206
|
+
* traversal, reused every render. This is the core of the performance
|
|
207
|
+
* optimization: instead of 10+ querySelectorAll calls per render, the scanner
|
|
208
|
+
* classifies every directive/event/ref attribute in a single DOM walk.
|
|
164
209
|
*
|
|
165
|
-
* LLM NOTE:
|
|
166
|
-
*
|
|
210
|
+
* LLM NOTE: ScanIndex is built lazily on first render and stored on
|
|
211
|
+
* `el.__micraScan`. Subsequent renders skip the scan entirely.
|
|
167
212
|
*/
|
|
168
|
-
export interface
|
|
213
|
+
export interface ScanIndex {
|
|
214
|
+
// Directives — applied on every render
|
|
169
215
|
text: CachedBinding[]
|
|
170
216
|
html: CachedBinding[]
|
|
171
217
|
if: CachedIfBinding[]
|
|
@@ -173,15 +219,27 @@ export interface DirectiveCache {
|
|
|
173
219
|
bind: CachedPairBinding[]
|
|
174
220
|
model: CachedBinding[]
|
|
175
221
|
class: CachedPairBinding[]
|
|
222
|
+
// Lists — <template data-each>
|
|
223
|
+
each: Element[]
|
|
224
|
+
// Events — bound once per element
|
|
225
|
+
on: Element[] // [data-on]
|
|
226
|
+
atEvents: Element[] // any element with at least one @-prefixed attribute
|
|
227
|
+
// Refs — collected into instance.refs every render
|
|
228
|
+
refs: Element[] // [data-ref]
|
|
176
229
|
}
|
|
177
230
|
|
|
178
231
|
/**
|
|
179
232
|
* @internal Full instance as seen inside the runtime — extends the public
|
|
180
|
-
*
|
|
233
|
+
* built-ins with private bookkeeping slots and an index signature for
|
|
181
234
|
* dynamic method dispatch.
|
|
235
|
+
*
|
|
236
|
+
* Note: internally we don't carry the user-method generic `M`. Internal modules
|
|
237
|
+
* dispatch methods by string name (`instance[methodName]()`), which is what
|
|
238
|
+
* the index signature is for. The public `mount()` return value re-projects
|
|
239
|
+
* to `ComponentInstance<S, M>` so callers get full type inference.
|
|
182
240
|
*/
|
|
183
241
|
export interface InternalInstance<S extends StateRecord = StateRecord>
|
|
184
|
-
extends
|
|
242
|
+
extends ComponentBuiltins<S> {
|
|
185
243
|
__micraSubs?: UnsubFn[]
|
|
186
244
|
__micraListeners?: TrackedListener[]
|
|
187
245
|
__micraDestroyed?: true
|
package/src/utils/expr.ts
CHANGED
|
@@ -29,8 +29,15 @@ import type { StateRecord } from '../types'
|
|
|
29
29
|
|
|
30
30
|
// LLM NOTE: exprCache is module-level (shared across all components).
|
|
31
31
|
// This is intentional — most apps reuse the same expressions.
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
|
|
33
|
+
// Compiled fn for complex expressions; pre-split parts for simple dot-paths.
|
|
34
|
+
// Storing parts once avoids the SIMPLE_PATH regex test + split on every evalExpr call.
|
|
35
|
+
type CompiledFn = (state: object, safe: object) => unknown
|
|
36
|
+
type CachedEntry =
|
|
37
|
+
| { kind: 'fn'; fn: CompiledFn }
|
|
38
|
+
| { kind: 'path'; parts: string[] }
|
|
39
|
+
|
|
40
|
+
const exprCache = new Map<string, CachedEntry>()
|
|
34
41
|
// Expressions whose runtime error we have already warned about. Prevents log spam
|
|
35
42
|
// when the same `data-text="item.naame"` typo fires every render.
|
|
36
43
|
const warnedRuntime = new Set<string>()
|
|
@@ -141,32 +148,38 @@ function safeStateHas(state: object, key: PropertyKey): boolean {
|
|
|
141
148
|
* evalExpr('price * qty', { price: 9.99, qty: 3 }) // → 29.97
|
|
142
149
|
*/
|
|
143
150
|
export function evalExpr(expr: string, state: StateRecord): unknown {
|
|
151
|
+
let cached = exprCache.get(expr)
|
|
152
|
+
|
|
153
|
+
if (!cached) {
|
|
154
|
+
// Determine once whether this is a simple dot-path and cache the result.
|
|
155
|
+
if (SIMPLE_PATH.test(expr)) {
|
|
156
|
+
cached = { kind: 'path', parts: expr.split('.') }
|
|
157
|
+
} else {
|
|
158
|
+
try {
|
|
159
|
+
cached = {
|
|
160
|
+
kind: 'fn',
|
|
161
|
+
fn: new Function('$s', '$safe', `with($safe){with($s){return (${expr})}}`) as CompiledFn,
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
warn(`invalid expression "${expr}"`)
|
|
165
|
+
cached = { kind: 'fn', fn: () => undefined }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
exprCache.set(expr, cached)
|
|
169
|
+
}
|
|
170
|
+
|
|
144
171
|
// Fast-path: simple property access — no Function() needed.
|
|
145
172
|
// Still guarded so bare access to Object.prototype names returns undefined.
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
obj != null ? (obj as StateRecord)[key] : undefined,
|
|
173
|
+
if (cached.kind === 'path') {
|
|
174
|
+
if (!safeStateHas(state, cached.parts[0]!)) return undefined
|
|
175
|
+
return cached.parts.reduce<unknown>(
|
|
176
|
+
(obj, key) => (obj != null ? (obj as StateRecord)[key] : undefined),
|
|
151
177
|
state,
|
|
152
178
|
)
|
|
153
179
|
}
|
|
154
180
|
|
|
155
|
-
if (!exprCache.has(expr)) {
|
|
156
|
-
try {
|
|
157
|
-
// Two with() statements: $s wins for state keys; $safe shadows globals.
|
|
158
|
-
exprCache.set(
|
|
159
|
-
expr,
|
|
160
|
-
new Function('$s', '$safe', `with($safe){with($s){return (${expr})}}`) as Compiled,
|
|
161
|
-
)
|
|
162
|
-
} catch {
|
|
163
|
-
warn(`invalid expression "${expr}"`)
|
|
164
|
-
exprCache.set(expr, () => undefined)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
181
|
try {
|
|
169
|
-
return
|
|
182
|
+
return cached.fn(safeStateWrap(state), SAFE_OUTER)
|
|
170
183
|
} catch (e) {
|
|
171
184
|
if (!warnedRuntime.has(expr)) {
|
|
172
185
|
warnedRuntime.add(expr)
|