@barefootjs/jsx 0.18.4 → 0.18.7
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/adapters/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST-HARNESS ONLY. Evaluate a signal initializer / prop-default source
|
|
3
|
+
* expression against a mock `props` object by EXECUTING it (`new
|
|
4
|
+
* Function`, with `undefined` shadowing the globals listed below), the
|
|
5
|
+
* same way the Hono/CSR conformance reference produces its value — by
|
|
6
|
+
* actually running the component. Never call this from a build path (`bf
|
|
7
|
+
* build`, `compileJSX`, any adapter's `generate`); it exists only so each
|
|
8
|
+
* adapter's `test-render.ts` conformance harness can seed a signal/
|
|
9
|
+
* prop-default for a non-JS-runtime SSR render (PHP/Ruby/Python/Perl/Go)
|
|
10
|
+
* that matches what Hono would compute. (#2209)
|
|
11
|
+
*
|
|
12
|
+
* NOT a security sandbox — the global shadowing below is a determinism
|
|
13
|
+
* tripwire (catches the common accidental-nondeterminism case, e.g.
|
|
14
|
+
* `Date.now()`), not containment: a sufficiently creative expression can
|
|
15
|
+
* still reach the real global scope (`new Function` bodies execute
|
|
16
|
+
* unlexically-scoped, so e.g. `[].constructor.constructor('return
|
|
17
|
+
* Date')()` recovers `Date` even though the bare name is shadowed, and
|
|
18
|
+
* `eval` can't be shadowed at all — JS forbids declaring or binding a
|
|
19
|
+
* parameter literally named `eval` in strict-mode code). Do not widen this
|
|
20
|
+
* module's use to anything but first-party fixture source. The actual
|
|
21
|
+
* trust boundary is that the evaluated text is first-party fixture
|
|
22
|
+
* source, already compiled by this same process — the conformance harness
|
|
23
|
+
* already executes fixture-derived code far more invasively (spawning
|
|
24
|
+
* `ruby`/`php`/`perl`/`go run` on generated programs). This module is
|
|
25
|
+
* never reachable from anything a real end user's untrusted input could
|
|
26
|
+
* influence.
|
|
27
|
+
*
|
|
28
|
+
* Why real execution instead of a hand-rolled evaluator: the initializer
|
|
29
|
+
* source is an arbitrary JS-subset expression over `props` (e.g. `(props.x
|
|
30
|
+
* ?? []).map(t => ({ ...t, editing: false }))`, #2209's actual repro) —
|
|
31
|
+
* every previous approach here was a regex/pattern-match over a small
|
|
32
|
+
* catalogue of recognized shapes (`props.x`, `props.x ?? default`, a bare
|
|
33
|
+
* literal), and #2209 is literally "the catalogue missed a shape" (the
|
|
34
|
+
* THIRD such miss on this codebase, per the superseded
|
|
35
|
+
* `evaluate-signal-init.test.ts`'s own #1672 pins). A hand-written
|
|
36
|
+
* evaluator over `ParsedExpr` would face the same drift, and can't
|
|
37
|
+
* represent object spread (`{ ...t, editing: false }`) without extending
|
|
38
|
+
* `ParsedExpr` — a production-compiler change that ripples into every
|
|
39
|
+
* adapter's exhaustive switch, disproportionate for a test-only need.
|
|
40
|
+
* `new Function` delegates parsing to the JS engine itself — CLAUDE.md's
|
|
41
|
+
* regex-parsing ban exists precisely to avoid the false-match/missed-shape
|
|
42
|
+
* failure mode this replaces.
|
|
43
|
+
*
|
|
44
|
+
* Shadowed as `undefined` (best-effort determinism, not containment — see
|
|
45
|
+
* above): `globalThis`, `window`, `document`, `Date`, `Math`, `crypto`,
|
|
46
|
+
* `performance`, `fetch`, `setTimeout`, `setInterval`, `require`,
|
|
47
|
+
* `process`, `Function`. (`eval` is deliberately absent from this list —
|
|
48
|
+
* it cannot be shadowed as a parameter name in strict-mode code; see
|
|
49
|
+
* above.)
|
|
50
|
+
*
|
|
51
|
+
* `props` bare-identifier destructured params (`createSignal(count)` where
|
|
52
|
+
* `count` is a destructured prop, not a `props.x` member) are NOT bound —
|
|
53
|
+
* the evaluator only exposes `props` — so such an initializer throws
|
|
54
|
+
* `ReferenceError` and falls back to "unset", matching every prior
|
|
55
|
+
* evaluator's behavior for that shape. Extending the environment with
|
|
56
|
+
* `ir.metadata.propsParams` bindings is a natural follow-up if a fixture
|
|
57
|
+
* ever needs it.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
const BLOCKED_GLOBALS = [
|
|
61
|
+
'globalThis',
|
|
62
|
+
'window',
|
|
63
|
+
'document',
|
|
64
|
+
'Date',
|
|
65
|
+
'Math',
|
|
66
|
+
'crypto',
|
|
67
|
+
'performance',
|
|
68
|
+
'fetch',
|
|
69
|
+
'setTimeout',
|
|
70
|
+
'setInterval',
|
|
71
|
+
'require',
|
|
72
|
+
'process',
|
|
73
|
+
'Function',
|
|
74
|
+
] as const
|
|
75
|
+
|
|
76
|
+
export type SignalInitEvalResult = { ok: true; value: unknown } | { ok: false }
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A value the harness's downstream language serializers (JSON/Python/PHP/
|
|
80
|
+
* Perl/Ruby literal builders) can actually marshal: `null`, a boolean, a
|
|
81
|
+
* number (including non-finite — several serializers already special-case
|
|
82
|
+
* NaN/Infinity), a string, a dense array with no holes and no `undefined`
|
|
83
|
+
* elements, or a plain object (`Object.prototype` or null prototype only —
|
|
84
|
+
* rejects class instances, functions, Maps/Sets, etc). Rejects genuine
|
|
85
|
+
* cycles; a shared (non-cyclic) reference appearing more than once — e.g.
|
|
86
|
+
* the same object at two array indices — is fine (JSON-equivalent
|
|
87
|
+
* behavior just duplicates it), so `ancestors` tracks only the current
|
|
88
|
+
* recursion path, not every value ever visited.
|
|
89
|
+
*/
|
|
90
|
+
function isTransportable(value: unknown, ancestors: Set<unknown> = new Set()): boolean {
|
|
91
|
+
if (value === null) return true
|
|
92
|
+
const t = typeof value
|
|
93
|
+
if (t === 'boolean' || t === 'number' || t === 'string') return true
|
|
94
|
+
if (t !== 'object') return false
|
|
95
|
+
if (ancestors.has(value)) return false // a real cycle (value is its own ancestor)
|
|
96
|
+
ancestors.add(value)
|
|
97
|
+
try {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
// `Array.prototype.every` silently skips holes (`[1, , 3]`), so a
|
|
100
|
+
// sparse array would otherwise pass — compare against the own
|
|
101
|
+
// enumerable key count (holes aren't own keys) to catch that.
|
|
102
|
+
if (Object.keys(value).length !== value.length) return false
|
|
103
|
+
return value.every(el => el !== undefined && isTransportable(el, ancestors))
|
|
104
|
+
}
|
|
105
|
+
const proto = Object.getPrototypeOf(value)
|
|
106
|
+
if (proto !== Object.prototype && proto !== null) return false
|
|
107
|
+
return Object.values(value as Record<string, unknown>).every(v => isTransportable(v, ancestors))
|
|
108
|
+
} finally {
|
|
109
|
+
ancestors.delete(value)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Evaluate `expr` (a JS-subset source expression) against `props`. Returns
|
|
115
|
+
* `{ ok: false }` when the expression fails to parse, throws at evaluation
|
|
116
|
+
* time (e.g. a `ReferenceError` for an unbound identifier), or evaluates to
|
|
117
|
+
* something the downstream serializers can't marshal (see
|
|
118
|
+
* {@link isTransportable}).
|
|
119
|
+
*/
|
|
120
|
+
export function tryEvaluateSignalInit(
|
|
121
|
+
expr: string,
|
|
122
|
+
props?: Record<string, unknown>,
|
|
123
|
+
): SignalInitEvalResult {
|
|
124
|
+
const src = expr.trim()
|
|
125
|
+
if (src === '') return { ok: false }
|
|
126
|
+
let fn: (props: Record<string, unknown>, ...blocked: undefined[]) => unknown
|
|
127
|
+
try {
|
|
128
|
+
// `props` plus the blocked-globals shadows are the only bindings
|
|
129
|
+
// passed in — best-effort determinism, not containment; see the file
|
|
130
|
+
// docstring for the trust-boundary rationale (test-harness-only,
|
|
131
|
+
// never a build path).
|
|
132
|
+
fn = new Function(
|
|
133
|
+
'props',
|
|
134
|
+
...BLOCKED_GLOBALS,
|
|
135
|
+
`'use strict'; return (\n${src}\n);`,
|
|
136
|
+
) as typeof fn
|
|
137
|
+
} catch {
|
|
138
|
+
return { ok: false }
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const value = fn(props ?? {})
|
|
142
|
+
// `undefined` at the TOP level is a genuine, distinguishable result
|
|
143
|
+
// (e.g. an explicit `undefined` initializer, or `props.x` with no `x`
|
|
144
|
+
// and no `??` fallback) — not a marshal failure. `evaluateSignalInit`'s
|
|
145
|
+
// wrapper still collapses it to the "skip" outcome; callers that need
|
|
146
|
+
// the distinction use this function directly.
|
|
147
|
+
if (value === undefined) return { ok: true, value: undefined }
|
|
148
|
+
return isTransportable(value) ? { ok: true, value } : { ok: false }
|
|
149
|
+
} catch {
|
|
150
|
+
return { ok: false }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Drop-in replacement for the harnesses' former per-adapter regex
|
|
156
|
+
* evaluator: `null` means "could not evaluate, or evaluated to
|
|
157
|
+
* `undefined` — leave the signal/default unseeded", matching every prior
|
|
158
|
+
* evaluator's convention (an explicit JS `null` initializer also maps to
|
|
159
|
+
* `null` — the same "skip" outcome, since none of these harnesses
|
|
160
|
+
* distinguish "explicitly null" from "unset" downstream).
|
|
161
|
+
*/
|
|
162
|
+
export function evaluateSignalInit(expr: string, props?: Record<string, unknown>): unknown {
|
|
163
|
+
const result = tryEvaluateSignalInit(expr, props)
|
|
164
|
+
return result.ok && result.value !== undefined ? result.value : null
|
|
165
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fully compile-time-known literal evaluation (#2208). A `ParsedExpr` that
|
|
3
|
+
* is a literal, a no-substitution (or fully-static) template literal, an
|
|
4
|
+
* array-literal of pure elements, or an object-literal of pure values
|
|
5
|
+
* evaluates here to its plain JS value. Consumed by each adapter's
|
|
6
|
+
* `renderLoop` to admit a function-scope local const whose initializer is
|
|
7
|
+
* entirely known at compile time as a loop source — inlining its value the
|
|
8
|
+
* same way a module-scope const already is — while still refusing a
|
|
9
|
+
* runtime-computed expression (`Object.entries(props.tags).filter(...)`,
|
|
10
|
+
* #2069) with BF101, since `call`/unresolvable-`member` shapes fall through
|
|
11
|
+
* to `null` below.
|
|
12
|
+
*
|
|
13
|
+
* Analysis only: this module never emits template syntax. Each adapter
|
|
14
|
+
* serializes the resolved JS value into its own native literal syntax
|
|
15
|
+
* (`packages/adapter-<name>/src/adapter/lib/static-value.ts`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ParsedExpr } from './expression-parser.ts'
|
|
19
|
+
import type { ConstantInfo } from './types.ts'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively evaluate a `ParsedExpr` that is a fully compile-time-known
|
|
23
|
+
* literal to its JS value. `bindings` lets a caller resolve identifiers
|
|
24
|
+
* (and member reads off them) against already-evaluated values — e.g. the
|
|
25
|
+
* Go adapter binding a loop's item parameter to one already-resolved array
|
|
26
|
+
* element while baking per-item child-component props. Returns `null` when
|
|
27
|
+
* the expression is not statically known (depends on a prop, signal,
|
|
28
|
+
* function call, or an identifier absent from `bindings`).
|
|
29
|
+
*/
|
|
30
|
+
export function evaluateStaticLiteral(
|
|
31
|
+
expr: ParsedExpr,
|
|
32
|
+
bindings?: ReadonlyMap<string, unknown>,
|
|
33
|
+
): { value: unknown } | null {
|
|
34
|
+
switch (expr.kind) {
|
|
35
|
+
case 'literal':
|
|
36
|
+
return { value: expr.value }
|
|
37
|
+
case 'template-literal': {
|
|
38
|
+
let out = ''
|
|
39
|
+
for (const part of expr.parts) {
|
|
40
|
+
if (part.type === 'string') {
|
|
41
|
+
out += part.value
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
const resolved = evaluateStaticLiteral(part.expr, bindings)
|
|
45
|
+
if (!resolved) return null
|
|
46
|
+
out += String(resolved.value)
|
|
47
|
+
}
|
|
48
|
+
return { value: out }
|
|
49
|
+
}
|
|
50
|
+
case 'array-literal': {
|
|
51
|
+
const values: unknown[] = []
|
|
52
|
+
for (const element of expr.elements) {
|
|
53
|
+
const resolved = evaluateStaticLiteral(element, bindings)
|
|
54
|
+
if (!resolved) return null
|
|
55
|
+
values.push(resolved.value)
|
|
56
|
+
}
|
|
57
|
+
return { value: values }
|
|
58
|
+
}
|
|
59
|
+
case 'object-literal': {
|
|
60
|
+
// Shorthand (`{ a }`) and explicit (`{ a: value }`) properties both
|
|
61
|
+
// carry their resolved tree in `value` (shorthand's is an
|
|
62
|
+
// `identifier`) — recursing here handles both uniformly: a shorthand
|
|
63
|
+
// property only resolves when `bindings` supplies it.
|
|
64
|
+
const out: Record<string, unknown> = {}
|
|
65
|
+
for (const prop of expr.properties) {
|
|
66
|
+
const resolved = evaluateStaticLiteral(prop.value, bindings)
|
|
67
|
+
if (!resolved) return null
|
|
68
|
+
out[prop.key] = resolved.value
|
|
69
|
+
}
|
|
70
|
+
return { value: out }
|
|
71
|
+
}
|
|
72
|
+
case 'unary': {
|
|
73
|
+
const resolved = evaluateStaticLiteral(expr.argument, bindings)
|
|
74
|
+
if (!resolved) return null
|
|
75
|
+
if (expr.op === '-') return typeof resolved.value === 'number' ? { value: -resolved.value } : null
|
|
76
|
+
if (expr.op === '+') return typeof resolved.value === 'number' ? { value: +resolved.value } : null
|
|
77
|
+
if (expr.op === '!') return { value: !resolved.value }
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
case 'identifier':
|
|
81
|
+
return bindings?.has(expr.name) ? { value: bindings.get(expr.name) } : null
|
|
82
|
+
case 'member': {
|
|
83
|
+
const base = evaluateStaticLiteral(expr.object, bindings)
|
|
84
|
+
if (!base || base.value === null || typeof base.value !== 'object') return null
|
|
85
|
+
return { value: (base.value as Record<string, unknown>)[expr.property] }
|
|
86
|
+
}
|
|
87
|
+
case 'index-access': {
|
|
88
|
+
const base = evaluateStaticLiteral(expr.object, bindings)
|
|
89
|
+
const index = evaluateStaticLiteral(expr.index, bindings)
|
|
90
|
+
if (!base || !index || !Array.isArray(base.value) || typeof index.value !== 'number') return null
|
|
91
|
+
return { value: base.value[index.value] }
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `true` iff {@link evaluateStaticLiteral} resolves `expr` with no bindings. */
|
|
99
|
+
export function isFullyStaticLiteral(expr: ParsedExpr): boolean {
|
|
100
|
+
return evaluateStaticLiteral(expr) !== null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Shared loop-source resolution for `renderLoop` across adapters: the loop
|
|
105
|
+
* array is either an inline array-literal, or a bare identifier naming a
|
|
106
|
+
* FUNCTION-scope (`!isModule`) local const whose initializer evaluates
|
|
107
|
+
* statically. Module-scope consts are deliberately excluded — an existing,
|
|
108
|
+
* separate seeding path already handles those. Returns the evaluated items,
|
|
109
|
+
* or `null` if the source isn't resolvable this way (including when it
|
|
110
|
+
* resolves to something other than an array).
|
|
111
|
+
*/
|
|
112
|
+
export function resolveStaticLoopSource(
|
|
113
|
+
arrayParsed: ParsedExpr | undefined,
|
|
114
|
+
localConstants: ReadonlyArray<ConstantInfo> | undefined,
|
|
115
|
+
opts?: { isNameShadowed?: (name: string) => boolean },
|
|
116
|
+
): unknown[] | null {
|
|
117
|
+
if (!arrayParsed) return null
|
|
118
|
+
let target = arrayParsed
|
|
119
|
+
if (arrayParsed.kind === 'identifier') {
|
|
120
|
+
if (opts?.isNameShadowed?.(arrayParsed.name)) return null
|
|
121
|
+
const local = localConstants?.find(c => c.name === arrayParsed.name)
|
|
122
|
+
if (!local || local.isModule || !local.parsed) return null
|
|
123
|
+
target = local.parsed
|
|
124
|
+
}
|
|
125
|
+
const resolved = evaluateStaticLiteral(target)
|
|
126
|
+
if (!resolved || !Array.isArray(resolved.value)) return null
|
|
127
|
+
return resolved.value
|
|
128
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -574,6 +574,74 @@ export interface IRLoop {
|
|
|
574
574
|
*/
|
|
575
575
|
iterationShape?: 'entries' | 'keys'
|
|
576
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Pre-`.map()` object iteration (#2168 object-entries-map). Distinct
|
|
579
|
+
* from {@link iterationShape}, which is scoped ENTIRELY to an array's
|
|
580
|
+
* own zero-arg `.entries()`/`.keys()`/`.values()` methods — those
|
|
581
|
+
* synthesize a real numeric index off the array's position, and every
|
|
582
|
+
* adapter's consumption of `iterationShape` assumes an actual
|
|
583
|
+
* array/slice underneath.
|
|
584
|
+
*
|
|
585
|
+
* `objectIteration` instead records the STATIC `Object.entries(x)` /
|
|
586
|
+
* `Object.keys(x)` / `Object.values(x)` call form, where `x` is a
|
|
587
|
+
* plain object/Record (not an array): the "index" bound for `'entries'`
|
|
588
|
+
* is a STRING KEY, not a numeric position, and the collection an
|
|
589
|
+
* adapter must iterate is its native map/dict/hash type, not an
|
|
590
|
+
* array/slice. `transformMapCall` strips the `Object.<method>(...)`
|
|
591
|
+
* wrapper the same way it strips `arr.entries()` — `array`/`arrayParsed`
|
|
592
|
+
* end up holding just `x` — and records the shape here so each
|
|
593
|
+
* adapter's loop renderer picks the right native construct:
|
|
594
|
+
*
|
|
595
|
+
* - `'entries'` → both `index` (bound to the KEY) and `param` (bound
|
|
596
|
+
* to the VALUE), synthesized from the 2-element destructure the
|
|
597
|
+
* same way `iterationShape: 'entries'` is (see `jsx-to-ir.ts`'s
|
|
598
|
+
* `transformMapCall`) — e.g. Jinja `for k, v in x.items()`.
|
|
599
|
+
* - `'keys'` → `param` bound to the key only — e.g. Jinja
|
|
600
|
+
* `for k in x.keys()`.
|
|
601
|
+
* - `'values'` → `param` bound to the value only — e.g. Jinja
|
|
602
|
+
* `for v in x.values()`. Unlike the array case, `'values'` is NOT
|
|
603
|
+
* a no-op here: `Object.values(x)` genuinely differs from
|
|
604
|
+
* iterating `x` itself (`x` isn't iterable at all as a plain
|
|
605
|
+
* object), so it must be recorded.
|
|
606
|
+
*
|
|
607
|
+
* Iteration ORDER is native-map-dependent: Python `dict`/PHP
|
|
608
|
+
* array-object/Ruby `Hash` preserve the source object's insertion
|
|
609
|
+
* order (matching JS `Object.entries()` semantics exactly), so Jinja,
|
|
610
|
+
* Twig, Blade, and ERB lower directly to their native map/dict/hash
|
|
611
|
+
* iteration. Go's `map[string]T`, Rust's `BTreeMap` (deliberate design,
|
|
612
|
+
* see `num.rs`), and Perl's hash (Xslate/Mojolicious) have NO
|
|
613
|
+
* order-preserving native map type, so those four instead lower to a
|
|
614
|
+
* DETERMINISTIC SORTED-BY-KEY iteration — Go's `{{range}}` (the
|
|
615
|
+
* stdlib's own `fmtsort`), minijinja's `BTreeMap` (already sorted),
|
|
616
|
+
* Kolon's `.kv()`/`.keys()`/`.values()` (verified empirically sorted),
|
|
617
|
+
* and Mojolicious's explicit `sort keys %$hash` (mirroring the
|
|
618
|
+
* existing `spread_attrs`/`_style_to_css` convention in
|
|
619
|
+
* `BarefootJS.pm`). This is a documented, permanent known limitation
|
|
620
|
+
* relative to JS's insertion-order guarantee (not a follow-up TODO) —
|
|
621
|
+
* true insertion order is physically unrecoverable from those
|
|
622
|
+
* languages' native map types once constructed, so sorted order is the
|
|
623
|
+
* best available deterministic approximation, not an interim refusal.
|
|
624
|
+
*
|
|
625
|
+
* Only the OUTERMOST, unchained `Object.<method>(x).map(cb)` shape is
|
|
626
|
+
* recognized — mirroring `iterationShape`'s own scope, chaining
|
|
627
|
+
* (`Object.entries(x).filter(pred).map(cb)`) is not (yet) recognized
|
|
628
|
+
* either, same as the array case.
|
|
629
|
+
*/
|
|
630
|
+
objectIteration?: 'entries' | 'keys' | 'values'
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Count of enclosing `.map()` loops (0 = outermost, 1 = nested one
|
|
634
|
+
* level deep, ...). Adapters use this to derive the loop body's
|
|
635
|
+
* `key`/`data-key` attribute suffix — `depth > 0 ? 'data-key-' +
|
|
636
|
+
* depth : 'data-key'` — matching `keyAttrName()` in
|
|
637
|
+
* `ir-to-client-js/utils.ts`, which the CSR path and the Hono SSR
|
|
638
|
+
* adapter each already derive independently (a recursion counter and
|
|
639
|
+
* a push/pop stack respectively). Before this field, the 8 template
|
|
640
|
+
* (non-JS) adapters had no depth awareness at all and always emitted
|
|
641
|
+
* plain `data-key` on nested-loop items (#2168 nested-loop-outer-binding).
|
|
642
|
+
*/
|
|
643
|
+
depth: number
|
|
644
|
+
|
|
577
645
|
/**
|
|
578
646
|
* When true, loop should be evaluated on client side only.
|
|
579
647
|
* SSR adapters should skip rendering and output placeholder markers.
|