micra.js 2.3.2 → 2.4.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/CHANGELOG.md +57 -0
- package/README.md +7 -4
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +296 -48
- package/dist/micra.cjs.js.map +3 -3
- package/dist/micra.esm.js +296 -48
- package/dist/micra.esm.js.map +3 -3
- package/dist/micra.js +296 -48
- package/dist/micra.js.map +3 -3
- package/dist/micra.min.js +3 -2
- package/dist/types.d.ts +1 -0
- package/dist/utils/expr.d.ts +29 -22
- package/llms-full.txt +19 -19
- package/llms.txt +3 -3
- package/package.json +2 -2
- package/src/core/mount.ts +4 -0
- package/src/dom/events.ts +37 -10
- package/src/index.ts +1 -1
- package/src/types.ts +1 -0
- package/src/utils/expr.ts +278 -121
package/src/utils/expr.ts
CHANGED
|
@@ -1,129 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* src/utils/expr.ts — JS
|
|
2
|
+
* src/utils/expr.ts — CSP-safe JS-expression evaluator.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - Shadow non-state identifiers so directive expressions cannot reach
|
|
9
|
-
* globals like `window`, `fetch`, `constructor`, etc. A small whitelist
|
|
10
|
-
* of utility globals (Math, JSON, Date, ...) remains accessible.
|
|
4
|
+
* Directive expressions (`data-text="count > 0"`, `data-class="x:a === b"`, …)
|
|
5
|
+
* are parsed into a small AST and walked by an interpreter. There is NO
|
|
6
|
+
* `new Function` / `eval` anywhere — so Micra runs under a strict
|
|
7
|
+
* Content-Security-Policy (`default-src 'self'`, no `unsafe-eval`).
|
|
11
8
|
*
|
|
12
9
|
* LLM NOTE: This module is PURE. It does not touch the DOM or mutate state.
|
|
13
10
|
*
|
|
14
11
|
* Security model:
|
|
15
|
-
*
|
|
16
|
-
* and
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
12
|
+
* The interpreter can only reach: top-level state keys, component methods,
|
|
13
|
+
* and a whitelist of utility globals (Math, JSON, Date, …). A bare
|
|
14
|
+
* identifier that is none of those resolves to `undefined` — `window`,
|
|
15
|
+
* `document`, `fetch`, `eval`, `constructor` are unreachable *by
|
|
16
|
+
* construction* (there is no scope that contains them), not by shadowing.
|
|
17
|
+
* Member access additionally refuses the prototype-escape property names
|
|
18
|
+
* (`__proto__`, `constructor`, `prototype`), closing the
|
|
19
|
+
* `item.constructor.constructor("…")()` chain that the old `with()`-based
|
|
20
|
+
* evaluator left open. Method calls still run real JS — if a component
|
|
21
|
+
* method touches `window`, that works; treat directive templates as
|
|
22
|
+
* trusted code regardless.
|
|
23
|
+
*
|
|
24
|
+
* Grammar (precedence low→high):
|
|
25
|
+
* ternary ?: | || | && | == != === !== | < <= > >= | + - |
|
|
26
|
+
* * / % | unary ! - | call() / member. | primary
|
|
27
|
+
* primary = number | string | true | false | null | undefined |
|
|
28
|
+
* identifier | ( expr )
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
31
|
import type { StateRecord } from '../types'
|
|
25
32
|
|
|
26
|
-
// ──
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
// ── Whitelisted globals ─────────────────────────────────────────────────────
|
|
34
|
+
const ALLOWED_GLOBALS = new Set<string>(
|
|
35
|
+
'Math,JSON,Date,String,Number,Boolean,Array,Object,parseInt,parseFloat,isNaN,isFinite,NaN,Infinity,undefined'.split(','),
|
|
36
|
+
)
|
|
29
37
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
38
|
+
// Property names that would let an expression climb back to the Function
|
|
39
|
+
// constructor / prototype. Blocked on every member access.
|
|
40
|
+
const BLOCKED_PROPS = new Set<string>(['__proto__', 'constructor', 'prototype'])
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
type CompiledFn = (state: object, safe: object) => unknown
|
|
36
|
-
type CachedEntry =
|
|
37
|
-
| { kind: 'fn'; fn: CompiledFn }
|
|
38
|
-
| { kind: 'path'; parts: string[] }
|
|
42
|
+
/** @internal Names that live on Object.prototype (constructor, toString, …). */
|
|
43
|
+
const OBJ_PROTO_KEYS = new Set<string>(Object.getOwnPropertyNames(Object.prototype))
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// ── AST ─────────────────────────────────────────────────────────────────────
|
|
46
|
+
// Compact node shapes; `k` is the kind tag.
|
|
47
|
+
type Node =
|
|
48
|
+
| { k: 'lit'; v: unknown }
|
|
49
|
+
| { k: 'id'; n: string }
|
|
50
|
+
| { k: 'mem'; o: Node; p: string }
|
|
51
|
+
| { k: 'call'; c: Node; a: Node[] }
|
|
52
|
+
| { k: 'un'; op: string; x: Node }
|
|
53
|
+
| { k: 'bin'; op: string; l: Node; r: Node }
|
|
54
|
+
| { k: 'tern'; c: Node; a: Node; b: Node }
|
|
44
55
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
const SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/
|
|
56
|
+
// ── Tokenizer ────────────────────────────────────────────────────────────────
|
|
57
|
+
type Tok = { t: 'num' | 'str' | 'id' | 'p'; v: string }
|
|
48
58
|
|
|
49
|
-
|
|
59
|
+
const PUNCT = [
|
|
60
|
+
'===', '!==', '==', '!=', '<=', '>=', '&&', '||',
|
|
61
|
+
'(', ')', '.', ',', '?', ':', '!', '<', '>', '+', '-', '*', '/', '%',
|
|
62
|
+
]
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
64
|
+
function tokenize(src: string): Tok[] {
|
|
65
|
+
const toks: Tok[] = []
|
|
66
|
+
let i = 0
|
|
67
|
+
const n = src.length
|
|
68
|
+
while (i < n) {
|
|
69
|
+
const c = src[i]!
|
|
70
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f') { i++; continue }
|
|
71
|
+
// string
|
|
72
|
+
if (c === '"' || c === "'") {
|
|
73
|
+
let s = ''
|
|
74
|
+
i++
|
|
75
|
+
while (i < n && src[i] !== c) {
|
|
76
|
+
if (src[i] === '\\') { s += src[i + 1] ?? ''; i += 2 }
|
|
77
|
+
else { s += src[i]; i++ }
|
|
78
|
+
}
|
|
79
|
+
if (src[i] !== c) throw 0 // unterminated
|
|
80
|
+
i++
|
|
81
|
+
toks.push({ t: 'str', v: s })
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
// number
|
|
85
|
+
if (c >= '0' && c <= '9') {
|
|
86
|
+
let s = ''
|
|
87
|
+
while (i < n && ((src[i]! >= '0' && src[i]! <= '9') || src[i] === '.')) { s += src[i]; i++ }
|
|
88
|
+
toks.push({ t: 'num', v: s })
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
// identifier
|
|
92
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
93
|
+
let s = ''
|
|
94
|
+
while (i < n && /[A-Za-z0-9_$]/.test(src[i]!)) { s += src[i]; i++ }
|
|
95
|
+
toks.push({ t: 'id', v: s })
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
// punctuator (longest match first)
|
|
99
|
+
const m = PUNCT.find(p => src.startsWith(p, i))
|
|
100
|
+
if (!m) throw 0 // unknown char
|
|
101
|
+
toks.push({ t: 'p', v: m })
|
|
102
|
+
i += m.length
|
|
103
|
+
}
|
|
104
|
+
return toks
|
|
105
|
+
}
|
|
58
106
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const PARAM_S = '$s'
|
|
68
|
-
const PARAM_SAFE = '$safe'
|
|
69
|
-
|
|
70
|
-
const SAFE_OUTER: object = new Proxy(Object.create(null) as object, {
|
|
71
|
-
has(_target, key): boolean {
|
|
72
|
-
if (typeof key !== 'string') return false
|
|
73
|
-
if (key === PARAM_S || key === PARAM_SAFE) return false
|
|
74
|
-
return !ALLOWED_GLOBALS.has(key)
|
|
75
|
-
},
|
|
76
|
-
get(): undefined {
|
|
77
|
-
return undefined
|
|
78
|
-
},
|
|
79
|
-
})
|
|
107
|
+
// ── Parser (Pratt) ────────────────────────────────────────────────────────────
|
|
108
|
+
const BIN_PREC: Record<string, number> = {
|
|
109
|
+
'||': 1, '&&': 2,
|
|
110
|
+
'==': 3, '!=': 3, '===': 3, '!==': 3,
|
|
111
|
+
'<': 4, '<=': 4, '>': 4, '>=': 4,
|
|
112
|
+
'+': 5, '-': 5,
|
|
113
|
+
'*': 6, '/': 6, '%': 6,
|
|
114
|
+
}
|
|
80
115
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
116
|
+
function parse(toks: Tok[]): Node {
|
|
117
|
+
let pos = 0
|
|
118
|
+
const peek = () => toks[pos]
|
|
119
|
+
const next = () => toks[pos++]
|
|
120
|
+
const eat = (v: string) => { if (peek()?.v !== v) throw 0; pos++ }
|
|
86
121
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
function parseExpr(): Node {
|
|
123
|
+
const c = parseBin(1)
|
|
124
|
+
if (peek()?.v === '?') {
|
|
125
|
+
next()
|
|
126
|
+
const a = parseExpr()
|
|
127
|
+
eat(':')
|
|
128
|
+
const b = parseExpr()
|
|
129
|
+
return { k: 'tern', c, a, b }
|
|
130
|
+
}
|
|
131
|
+
return c
|
|
132
|
+
}
|
|
93
133
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
function parseBin(minPrec: number): Node {
|
|
135
|
+
let left = parseUnary()
|
|
136
|
+
for (;;) {
|
|
137
|
+
const t = peek()
|
|
138
|
+
const prec = t && t.t === 'p' ? BIN_PREC[t.v] : undefined
|
|
139
|
+
if (prec === undefined || prec < minPrec) break
|
|
140
|
+
next()
|
|
141
|
+
const right = parseBin(prec + 1)
|
|
142
|
+
left = { k: 'bin', op: t!.v, l: left, r: right }
|
|
143
|
+
}
|
|
144
|
+
return left
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseUnary(): Node {
|
|
148
|
+
const t = peek()
|
|
149
|
+
if (t && t.t === 'p' && (t.v === '!' || t.v === '-')) {
|
|
150
|
+
next()
|
|
151
|
+
return { k: 'un', op: t.v, x: parseUnary() }
|
|
152
|
+
}
|
|
153
|
+
return parsePostfix()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parsePostfix(): Node {
|
|
157
|
+
let node = parsePrimary()
|
|
158
|
+
for (;;) {
|
|
159
|
+
const t = peek()
|
|
160
|
+
if (t?.v === '.') {
|
|
161
|
+
next()
|
|
162
|
+
const id = next()
|
|
163
|
+
if (!id || id.t !== 'id') throw 0
|
|
164
|
+
node = { k: 'mem', o: node, p: id.v }
|
|
165
|
+
} else if (t?.v === '(') {
|
|
166
|
+
next()
|
|
167
|
+
const args: Node[] = []
|
|
168
|
+
if (peek()?.v !== ')') {
|
|
169
|
+
args.push(parseExpr())
|
|
170
|
+
while (peek()?.v === ',') { next(); args.push(parseExpr()) }
|
|
171
|
+
}
|
|
172
|
+
eat(')')
|
|
173
|
+
node = { k: 'call', c: node, a: args }
|
|
174
|
+
} else break
|
|
175
|
+
}
|
|
176
|
+
return node
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parsePrimary(): Node {
|
|
180
|
+
const t = next()
|
|
181
|
+
if (!t) throw 0
|
|
182
|
+
if (t.t === 'num') return { k: 'lit', v: Number(t.v) }
|
|
183
|
+
if (t.t === 'str') return { k: 'lit', v: t.v }
|
|
184
|
+
if (t.v === '(') { const e = parseExpr(); eat(')'); return e }
|
|
185
|
+
if (t.t === 'id') {
|
|
186
|
+
if (t.v === 'true') return { k: 'lit', v: true }
|
|
187
|
+
if (t.v === 'false') return { k: 'lit', v: false }
|
|
188
|
+
if (t.v === 'null') return { k: 'lit', v: null }
|
|
189
|
+
if (t.v === 'undefined') return { k: 'lit', v: undefined }
|
|
190
|
+
return { k: 'id', n: t.v }
|
|
191
|
+
}
|
|
192
|
+
throw 0
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const ast = parseExpr()
|
|
196
|
+
if (pos !== toks.length) throw 0 // trailing garbage
|
|
197
|
+
return ast
|
|
112
198
|
}
|
|
113
199
|
|
|
200
|
+
// ── Identifier resolution ─────────────────────────────────────────────────────
|
|
201
|
+
|
|
114
202
|
/**
|
|
115
203
|
* Return true iff `key` is reachable on `state` without walking into
|
|
116
|
-
* `Object.prototype`.
|
|
117
|
-
*
|
|
204
|
+
* `Object.prototype`. Blocks `'constructor' in state` from leaking the
|
|
205
|
+
* prototype while still allowing user-defined keys that happen to share a
|
|
206
|
+
* built-in name.
|
|
118
207
|
*/
|
|
119
|
-
function safeStateHas(state: object, key:
|
|
120
|
-
if (typeof key !== 'string') return false
|
|
208
|
+
function safeStateHas(state: object, key: string): boolean {
|
|
121
209
|
if (!Reflect.has(state, key)) return false
|
|
122
|
-
// Identifiers that are NOT on Object.prototype are always safe — accept them
|
|
123
|
-
// immediately without walking the chain.
|
|
124
210
|
if (!OBJ_PROTO_KEYS.has(key)) return true
|
|
125
|
-
// Built-in Object.prototype names (constructor, toString, hasOwnProperty, ...)
|
|
126
|
-
// are only accepted when they have been explicitly placed on the state chain.
|
|
127
211
|
let obj: object | null = state
|
|
128
212
|
while (obj && obj !== Object.prototype) {
|
|
129
213
|
if (Object.prototype.hasOwnProperty.call(obj, key)) return true
|
|
@@ -132,53 +216,126 @@ function safeStateHas(state: object, key: PropertyKey): boolean {
|
|
|
132
216
|
return false
|
|
133
217
|
}
|
|
134
218
|
|
|
135
|
-
|
|
219
|
+
function resolveIdent(name: string, scope: StateRecord): unknown {
|
|
220
|
+
if (safeStateHas(scope, name)) return scope[name]
|
|
221
|
+
if (ALLOWED_GLOBALS.has(name)) return (globalThis as Record<string, unknown>)[name]
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Interpreter ────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function evalNode(node: Node, scope: StateRecord): unknown {
|
|
228
|
+
switch (node.k) {
|
|
229
|
+
case 'lit': return node.v
|
|
230
|
+
case 'id': return resolveIdent(node.n, scope)
|
|
231
|
+
case 'mem': {
|
|
232
|
+
const o = evalNode(node.o, scope)
|
|
233
|
+
if (o == null || BLOCKED_PROPS.has(node.p)) return undefined
|
|
234
|
+
return (o as Record<string, unknown>)[node.p]
|
|
235
|
+
}
|
|
236
|
+
case 'un': {
|
|
237
|
+
const x = evalNode(node.x, scope)
|
|
238
|
+
return node.op === '!' ? !x : -(x as number)
|
|
239
|
+
}
|
|
240
|
+
case 'tern':
|
|
241
|
+
return evalNode(node.c, scope) ? evalNode(node.a, scope) : evalNode(node.b, scope)
|
|
242
|
+
case 'bin': {
|
|
243
|
+
const op = node.op
|
|
244
|
+
// short-circuit logicals (return operand value, JS semantics)
|
|
245
|
+
if (op === '&&') { const l = evalNode(node.l, scope); return l ? evalNode(node.r, scope) : l }
|
|
246
|
+
if (op === '||') { const l = evalNode(node.l, scope); return l ? l : evalNode(node.r, scope) }
|
|
247
|
+
const l = evalNode(node.l, scope) as never
|
|
248
|
+
const r = evalNode(node.r, scope) as never
|
|
249
|
+
switch (op) {
|
|
250
|
+
case '+': return (l as number) + (r as number)
|
|
251
|
+
case '-': return (l as number) - (r as number)
|
|
252
|
+
case '*': return (l as number) * (r as number)
|
|
253
|
+
case '/': return (l as number) / (r as number)
|
|
254
|
+
case '%': return (l as number) % (r as number)
|
|
255
|
+
case '<': return l < r
|
|
256
|
+
case '<=': return l <= r
|
|
257
|
+
case '>': return l > r
|
|
258
|
+
case '>=': return l >= r
|
|
259
|
+
case '==': return l == r
|
|
260
|
+
case '!=': return l != r
|
|
261
|
+
case '===': return l === r
|
|
262
|
+
case '!==': return l !== r
|
|
263
|
+
}
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
case 'call': {
|
|
267
|
+
// member call binds `this` to the object (item.fmt(), Math.round(x));
|
|
268
|
+
// bare call leaves `this` undefined (instance methods are pre-bound).
|
|
269
|
+
let fn: unknown
|
|
270
|
+
let self: unknown
|
|
271
|
+
if (node.c.k === 'mem') {
|
|
272
|
+
self = evalNode(node.c.o, scope)
|
|
273
|
+
fn = self == null || BLOCKED_PROPS.has(node.c.p)
|
|
274
|
+
? undefined
|
|
275
|
+
: (self as Record<string, unknown>)[node.c.p]
|
|
276
|
+
} else {
|
|
277
|
+
fn = evalNode(node.c, scope)
|
|
278
|
+
}
|
|
279
|
+
if (typeof fn !== 'function') throw new TypeError('not a function')
|
|
280
|
+
return (fn as (...a: unknown[]) => unknown).apply(self, node.a.map(x => evalNode(x, scope)))
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Cache + public API ──────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
type CachedEntry =
|
|
288
|
+
| { kind: 'path'; parts: string[] }
|
|
289
|
+
| { kind: 'ast'; ast: Node }
|
|
290
|
+
| { kind: 'err' }
|
|
291
|
+
|
|
292
|
+
const exprCache = new Map<string, CachedEntry>()
|
|
293
|
+
const warnedRuntime = new Set<string>()
|
|
294
|
+
|
|
295
|
+
// Simple identifier or dot-path: "count", "user.name", "item.email".
|
|
296
|
+
const SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/
|
|
136
297
|
|
|
137
298
|
/**
|
|
138
|
-
* Evaluate
|
|
299
|
+
* Evaluate an expression string against a state object.
|
|
139
300
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
301
|
+
* Cached by string. Simple dot-paths take a fast path that skips tokenizing.
|
|
302
|
+
* Parse errors warn once and thereafter resolve to `undefined`; runtime
|
|
303
|
+
* errors (e.g. calling a non-function) warn once per expression.
|
|
143
304
|
*
|
|
144
305
|
* @example
|
|
145
|
-
* evalExpr('count > 0', { count: 5 })
|
|
146
|
-
* evalExpr('user.name', { user: { name: 'Alice' } })
|
|
147
|
-
* evalExpr('price * qty', { price: 9.99, qty: 3 })
|
|
306
|
+
* evalExpr('count > 0', { count: 5 }) // → true
|
|
307
|
+
* evalExpr('user.name', { user: { name: 'Alice' } }) // → 'Alice'
|
|
308
|
+
* evalExpr('price * qty', { price: 9.99, qty: 3 }) // → 29.97
|
|
148
309
|
*/
|
|
149
310
|
export function evalExpr(expr: string, state: StateRecord): unknown {
|
|
150
311
|
let cached = exprCache.get(expr)
|
|
151
312
|
|
|
152
313
|
if (!cached) {
|
|
153
|
-
// Determine once whether this is a simple dot-path and cache the result.
|
|
154
314
|
if (SIMPLE_PATH.test(expr)) {
|
|
155
315
|
cached = { kind: 'path', parts: expr.split('.') }
|
|
156
316
|
} else {
|
|
157
317
|
try {
|
|
158
|
-
cached = {
|
|
159
|
-
kind: 'fn',
|
|
160
|
-
fn: new Function('$s', '$safe', `with($safe){with($s){return (${expr})}}`) as CompiledFn,
|
|
161
|
-
}
|
|
318
|
+
cached = { kind: 'ast', ast: parse(tokenize(expr)) }
|
|
162
319
|
} catch {
|
|
163
320
|
warn(`invalid expression "${expr}"`)
|
|
164
|
-
cached = { kind: '
|
|
321
|
+
cached = { kind: 'err' }
|
|
165
322
|
}
|
|
166
323
|
}
|
|
167
324
|
exprCache.set(expr, cached)
|
|
168
325
|
}
|
|
169
326
|
|
|
170
|
-
// Fast-path: simple property access — no Function() needed.
|
|
171
|
-
// Still guarded so bare access to Object.prototype names returns undefined.
|
|
172
327
|
if (cached.kind === 'path') {
|
|
173
|
-
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
328
|
+
const parts = cached.parts
|
|
329
|
+
if (!safeStateHas(state, parts[0]!)) return undefined
|
|
330
|
+
let obj: unknown = state
|
|
331
|
+
for (const key of parts) obj = obj != null ? (obj as StateRecord)[key] : undefined
|
|
332
|
+
return obj
|
|
178
333
|
}
|
|
179
334
|
|
|
335
|
+
if (cached.kind === 'err') return undefined
|
|
336
|
+
|
|
180
337
|
try {
|
|
181
|
-
return cached.
|
|
338
|
+
return evalNode(cached.ast, state)
|
|
182
339
|
} catch (e) {
|
|
183
340
|
if (!warnedRuntime.has(expr)) {
|
|
184
341
|
warnedRuntime.add(expr)
|