@barefootjs/jsx 0.18.4 → 0.18.5
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/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.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.js +141 -44
- 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/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/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +1 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +9 -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/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/adapters/parsed-expr-emitter.ts +10 -1
- package/src/expression-parser.ts +59 -25
- package/src/ir-to-client-js/collect-elements.ts +3 -0
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/csr-substitute.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +57 -11
- package/src/ir-to-client-js/types.ts +10 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +92 -9
- package/src/types.ts +68 -0
package/src/expression-parser.ts
CHANGED
|
@@ -25,7 +25,21 @@ export type ParsedExpr =
|
|
|
25
25
|
// canonical form in `value`.
|
|
26
26
|
| { kind: 'literal'; value: string | number | boolean | null; literalType: 'string' | 'number' | 'boolean' | 'null'; raw?: string }
|
|
27
27
|
| { kind: 'call'; callee: ParsedExpr; args: ParsedExpr[] }
|
|
28
|
-
|
|
28
|
+
// `optional` is true for a `?.`-written access (`user?.name`,
|
|
29
|
+
// `arr?.[0]`) — the ONE hop that was actually optional-chained in JS
|
|
30
|
+
// source; a plain `.`/`[]` access is `false`. Adapters whose member
|
|
31
|
+
// lowering is already null-safe by construction (e.g. Jinja's `[]`
|
|
32
|
+
// subscript swallows a `None` receiver, Blade's `data_get` helper)
|
|
33
|
+
// ignore this field entirely; Go and ERB (whose native `.` access
|
|
34
|
+
// panics/raises on a nil/undefined receiver) route an `optional: true`
|
|
35
|
+
// access through a defensive form (`bf_get`, Ruby `&.`) instead. NOTE:
|
|
36
|
+
// this does NOT reproduce JS's whole-chain short-circuit for a
|
|
37
|
+
// MULTI-hop chain (`a?.b.c` also guards `.c` in JS once `a` is
|
|
38
|
+
// nullish) — only the single hop written with `?.` is marked; a
|
|
39
|
+
// following plain `.c` on a possibly-nullish `a?.b` is a known,
|
|
40
|
+
// untracked limitation on Go/ERB (not exercised by the current
|
|
41
|
+
// `optional-chaining-prop` fixture, which is single-hop only).
|
|
42
|
+
| { kind: 'member'; object: ParsedExpr; property: string; computed: boolean; optional: boolean }
|
|
29
43
|
// Element access with a NON-literal index (`selected()[index]`,
|
|
30
44
|
// `rows[i + 1]`). A literal-index access (`arr[0]`, `obj['key']`)
|
|
31
45
|
// stays a `member` (computed) since the key is statically known and
|
|
@@ -88,11 +102,14 @@ export type ParsedExpr =
|
|
|
88
102
|
| 'toLowerCase'
|
|
89
103
|
| 'toUpperCase'
|
|
90
104
|
| 'trim'
|
|
105
|
+
| 'trimStart'
|
|
106
|
+
| 'trimEnd'
|
|
91
107
|
| 'toFixed'
|
|
92
108
|
| 'split'
|
|
93
109
|
| 'startsWith'
|
|
94
110
|
| 'endsWith'
|
|
95
111
|
| 'replace'
|
|
112
|
+
| 'replaceAll'
|
|
96
113
|
| 'repeat'
|
|
97
114
|
| 'padStart'
|
|
98
115
|
| 'padEnd'
|
|
@@ -345,18 +362,17 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
345
362
|
// `startsWith` / `endsWith` are no longer here — both lower via the
|
|
346
363
|
// `array-method` IR + `bf_starts_with` / `bf_ends_with` (Go) and
|
|
347
364
|
// `bf->starts_with` / `bf->ends_with` (Mojo). See #1448 Tier B.
|
|
348
|
-
// `replace`
|
|
349
|
-
// the `array-method` IR + `bf_replace`
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
365
|
+
// `replace` / `replaceAll` are no longer here — the string-pattern
|
|
366
|
+
// form of each lowers via the `array-method` IR + `bf_replace` /
|
|
367
|
+
// `bf_replace_all` (Go) / `bf->replace` / `bf->replace_all` (Mojo);
|
|
368
|
+
// the regex-pattern form of EITHER is refused at the parse arm below
|
|
369
|
+
// (it would need the per-adapter regex-flavour decision).
|
|
353
370
|
// `repeat` is no longer here — `String.prototype.repeat(n)` lowers via
|
|
354
371
|
// the `array-method` IR + `bf_repeat` (Go) / `bf->repeat` (Mojo).
|
|
355
372
|
// See #1448 Tier B.
|
|
356
373
|
// `padStart` / `padEnd` are no longer here — both lower via the
|
|
357
374
|
// `array-method` IR + `bf_pad_start` / `bf_pad_end` (Go) and
|
|
358
375
|
// `bf->pad_start` / `bf->pad_end` (Mojo). See #1448 Tier B.
|
|
359
|
-
'replaceAll',
|
|
360
376
|
'charAt', 'charCodeAt', 'codePointAt', 'normalize',
|
|
361
377
|
'substring', 'substr', 'match', 'matchAll', 'search',
|
|
362
378
|
])
|
|
@@ -882,6 +898,16 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
882
898
|
if (callee.property === 'trim') {
|
|
883
899
|
return { kind: 'array-method', method: 'trim', object: callee.object, args }
|
|
884
900
|
}
|
|
901
|
+
// `.trimStart()` / `.trimEnd()` — the one-sided siblings of
|
|
902
|
+
// `.trim()` (#2183 follow-up). No array equivalent exists, so
|
|
903
|
+
// there's no receiver-type ambiguity to resolve (unlike `.slice`);
|
|
904
|
+
// each is a dedicated `array-method` variant (matching the
|
|
905
|
+
// `padStart`/`padEnd` precedent — separate members, not a shared
|
|
906
|
+
// member with a `side` flag) lowering to its own runtime helper
|
|
907
|
+
// per adapter, not `.trim()` with a flag.
|
|
908
|
+
if (callee.property === 'trimStart' || callee.property === 'trimEnd') {
|
|
909
|
+
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
910
|
+
}
|
|
885
911
|
// `.toFixed(digits?)` — Number → fixed-decimal string. The digit
|
|
886
912
|
// count (default 0) travels as the single arg; all adapters route
|
|
887
913
|
// through a `to_fixed` runtime helper (Perl) / `fmt.Sprintf` (Go)
|
|
@@ -954,19 +980,25 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
954
980
|
// refused: `isSupported` maps a regex-pattern `.replace` to the
|
|
955
981
|
// deferred-form BF101 reason — the Perl `s///` vs Go
|
|
956
982
|
// `regexp.ReplaceAllString` flavour gap is the open design
|
|
957
|
-
// question in #1448.
|
|
983
|
+
// question in #1448. `.replaceAll(pattern, replacement)` shares
|
|
984
|
+
// this arm (#2182 follow-up): same arity/regex/object-literal
|
|
985
|
+
// gates, only the `method` tag and its runtime helper differ —
|
|
986
|
+
// `bf_replace_all` (Go, `strings.ReplaceAll`) / `bf->replace_all`
|
|
987
|
+
// (Mojo) replace EVERY occurrence, where `.replace`'s helpers
|
|
988
|
+
// replace only the first.
|
|
958
989
|
//
|
|
959
990
|
// Full JS arity: a third+ argument is ignored (the adapter reads
|
|
960
991
|
// only the pattern + replacement). The one- and zero-argument
|
|
961
992
|
// forms are refused: JS coerces the missing replacement (and
|
|
962
993
|
// pattern) to the literal string "undefined", a degenerate result
|
|
963
994
|
// (mirrors the `.includes()` / `.startsWith()` zero-arg refusal).
|
|
964
|
-
if (callee.property === 'replace') {
|
|
995
|
+
if (callee.property === 'replace' || callee.property === 'replaceAll') {
|
|
996
|
+
const method = callee.property
|
|
965
997
|
if (args.length < 2) {
|
|
966
998
|
return {
|
|
967
999
|
kind: 'unsupported',
|
|
968
1000
|
raw,
|
|
969
|
-
reason:
|
|
1001
|
+
reason: `\`.${method}(${args.length === 0 ? '' : 'pattern'})\` needs both a pattern and a replacement — JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`,
|
|
970
1002
|
}
|
|
971
1003
|
}
|
|
972
1004
|
// A regex-literal pattern is the deferred form (the Perl `s///`
|
|
@@ -980,7 +1012,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
980
1012
|
// any template use with the deferred-form reason.
|
|
981
1013
|
const patternNode = node.arguments[0]
|
|
982
1014
|
if (patternNode && ts.isRegularExpressionLiteral(patternNode)) {
|
|
983
|
-
return { kind: 'array-method', method
|
|
1015
|
+
return { kind: 'array-method', method, object: callee.object, args }
|
|
984
1016
|
}
|
|
985
1017
|
// Treat an object-literal argument like `unsupported` — a `.replace`
|
|
986
1018
|
// with an object pattern/replacement isn't lowerable, same as before
|
|
@@ -995,7 +1027,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
995
1027
|
const reason = badArg.kind === 'unsupported' ? badArg.reason : 'Unsupported syntax: ObjectLiteralExpression'
|
|
996
1028
|
return { kind: 'unsupported', raw, reason }
|
|
997
1029
|
}
|
|
998
|
-
return { kind: 'array-method', method
|
|
1030
|
+
return { kind: 'array-method', method, object: callee.object, args }
|
|
999
1031
|
}
|
|
1000
1032
|
// `.repeat(n)` — string → string (the receiver concatenated `n`
|
|
1001
1033
|
// times). Go uses `bf_repeat` (`strings.Repeat`, clamping a
|
|
@@ -1072,7 +1104,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1072
1104
|
const property = node.name.text
|
|
1073
1105
|
|
|
1074
1106
|
// Return as normal member - filter.length is handled in adapter
|
|
1075
|
-
return { kind: 'member', object, property, computed: false }
|
|
1107
|
+
return { kind: 'member', object, property, computed: false, optional: !!node.questionDotToken }
|
|
1076
1108
|
}
|
|
1077
1109
|
|
|
1078
1110
|
// Element access: items[0], obj['key']
|
|
@@ -1088,10 +1120,10 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1088
1120
|
}
|
|
1089
1121
|
// For simple number/string access, store as property
|
|
1090
1122
|
if (ts.isNumericLiteral(argNode)) {
|
|
1091
|
-
return { kind: 'member', object, property: argNode.text, computed: true }
|
|
1123
|
+
return { kind: 'member', object, property: argNode.text, computed: true, optional: !!node.questionDotToken }
|
|
1092
1124
|
}
|
|
1093
1125
|
if (ts.isStringLiteral(argNode)) {
|
|
1094
|
-
return { kind: 'member', object, property: argNode.text, computed: true }
|
|
1126
|
+
return { kind: 'member', object, property: argNode.text, computed: true, optional: !!node.questionDotToken }
|
|
1095
1127
|
}
|
|
1096
1128
|
// Variable / expression index (`selected()[index]`, `rows[i + 1]`):
|
|
1097
1129
|
// carry the index as its own ParsedExpr so the adapter can lower it
|
|
@@ -2063,7 +2095,7 @@ function substituteDestructuredFields(
|
|
|
2063
2095
|
// one-hop chain identical to the pre-#1530 shape.
|
|
2064
2096
|
let node: ParsedExpr = { kind: 'identifier', name: syntheticParam }
|
|
2065
2097
|
for (const segment of entry.path) {
|
|
2066
|
-
node = { kind: 'member', object: node, property: segment, computed: false }
|
|
2098
|
+
node = { kind: 'member', object: node, property: segment, computed: false, optional: false }
|
|
2067
2099
|
}
|
|
2068
2100
|
// Default value (#1531): wrap the accessor in `?? <default>` so
|
|
2069
2101
|
// a missing field falls back to the user-supplied literal /
|
|
@@ -2088,9 +2120,10 @@ function substituteDestructuredFields(
|
|
|
2088
2120
|
object: { kind: 'identifier', name: syntheticParam },
|
|
2089
2121
|
property: e.property,
|
|
2090
2122
|
computed: false,
|
|
2123
|
+
optional: e.optional,
|
|
2091
2124
|
}
|
|
2092
2125
|
}
|
|
2093
|
-
return { kind: 'member', object: walk(e.object), property: e.property, computed: e.computed }
|
|
2126
|
+
return { kind: 'member', object: walk(e.object), property: e.property, computed: e.computed, optional: e.optional }
|
|
2094
2127
|
case 'index-access':
|
|
2095
2128
|
return { kind: 'index-access', object: walk(e.object), index: walk(e.index) }
|
|
2096
2129
|
case 'binary':
|
|
@@ -2236,15 +2269,16 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2236
2269
|
}
|
|
2237
2270
|
|
|
2238
2271
|
case 'array-method': {
|
|
2239
|
-
// A regex-pattern `.replace` is carried structurally
|
|
2240
|
-
// arg) but is the deferred form (#1448) — no
|
|
2241
|
-
// Refuse with the dedicated reason
|
|
2242
|
-
//
|
|
2243
|
-
|
|
2272
|
+
// A regex-pattern `.replace` / `.replaceAll` is carried structurally
|
|
2273
|
+
// (a `regex` first arg) but is the deferred form (#1448) — no
|
|
2274
|
+
// template language lowers it. Refuse with the dedicated reason
|
|
2275
|
+
// rather than the generic standalone-regex message, preserving the
|
|
2276
|
+
// diagnostic the parser used to emit directly.
|
|
2277
|
+
if ((expr.method === 'replace' || expr.method === 'replaceAll') && expr.args[0]?.kind === 'regex') {
|
|
2244
2278
|
return {
|
|
2245
2279
|
supported: false,
|
|
2246
2280
|
reason:
|
|
2247
|
-
|
|
2281
|
+
`String.prototype.${expr.method} supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */`,
|
|
2248
2282
|
}
|
|
2249
2283
|
}
|
|
2250
2284
|
const objSupport = checkSupport(expr.object)
|
|
@@ -2893,7 +2927,7 @@ function inlineBinding(
|
|
|
2893
2927
|
case 'call':
|
|
2894
2928
|
return { kind: 'call', callee: walk(e.callee, enclosing), args: e.args.map(a => walk(a, enclosing)) }
|
|
2895
2929
|
case 'member':
|
|
2896
|
-
return { kind: 'member', object: walk(e.object, enclosing), property: e.property, computed: e.computed }
|
|
2930
|
+
return { kind: 'member', object: walk(e.object, enclosing), property: e.property, computed: e.computed, optional: e.optional }
|
|
2897
2931
|
case 'index-access':
|
|
2898
2932
|
return { kind: 'index-access', object: walk(e.object, enclosing), index: walk(e.index, enclosing) }
|
|
2899
2933
|
case 'binary':
|
|
@@ -3281,7 +3315,7 @@ export function materializeGetterCalls(expr: ParsedExpr, names: ReadonlySet<stri
|
|
|
3281
3315
|
alternate: rw(expr.alternate),
|
|
3282
3316
|
}
|
|
3283
3317
|
case 'member':
|
|
3284
|
-
return { kind: 'member', object: rw(expr.object), property: expr.property, computed: expr.computed }
|
|
3318
|
+
return { kind: 'member', object: rw(expr.object), property: expr.property, computed: expr.computed, optional: expr.optional }
|
|
3285
3319
|
case 'index-access':
|
|
3286
3320
|
return { kind: 'index-access', object: rw(expr.object), index: rw(expr.index) }
|
|
3287
3321
|
case 'template-literal':
|
|
@@ -390,6 +390,7 @@ export function collectInnerLoops(
|
|
|
390
390
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
391
391
|
bodyIsItemConditional: n.bodyIsItemConditional,
|
|
392
392
|
iterationShape: n.iterationShape,
|
|
393
|
+
objectIteration: n.objectIteration,
|
|
393
394
|
containerSlotId: scope.parentSlotId,
|
|
394
395
|
template,
|
|
395
396
|
mapPreamble: n.mapPreamble,
|
|
@@ -743,6 +744,7 @@ export function collectElements(
|
|
|
743
744
|
bodyIsMultiRoot: l.bodyIsMultiRoot,
|
|
744
745
|
bodyIsItemConditional: l.bodyIsItemConditional,
|
|
745
746
|
iterationShape: l.iterationShape,
|
|
747
|
+
objectIteration: l.objectIteration,
|
|
746
748
|
template,
|
|
747
749
|
staticItemTemplate,
|
|
748
750
|
skeletonTemplate,
|
|
@@ -1096,6 +1098,7 @@ function collectBranchLoops(
|
|
|
1096
1098
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
1097
1099
|
bodyIsItemConditional: n.bodyIsItemConditional,
|
|
1098
1100
|
iterationShape: n.iterationShape,
|
|
1101
|
+
objectIteration: n.objectIteration,
|
|
1099
1102
|
template: childTemplate,
|
|
1100
1103
|
containerSlotId: containerSlot,
|
|
1101
1104
|
mapPreamble: n.mapPreamble ?? null,
|
|
@@ -34,6 +34,7 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
34
34
|
param: elem.param,
|
|
35
35
|
paramBindings: elem.paramBindings,
|
|
36
36
|
key: elem.key,
|
|
37
|
+
index: elem.index,
|
|
37
38
|
mapPreamble: elem.mapPreamble ?? null,
|
|
38
39
|
}),
|
|
39
40
|
}
|
|
@@ -58,6 +59,7 @@ export function buildBranchLoopDelegationPlan(
|
|
|
58
59
|
param: loop.param,
|
|
59
60
|
paramBindings: loop.paramBindings,
|
|
60
61
|
key: loop.key,
|
|
62
|
+
index: loop.index,
|
|
61
63
|
mapPreamble: loop.mapPreamble ?? null,
|
|
62
64
|
}),
|
|
63
65
|
}
|
|
@@ -86,6 +88,7 @@ export function buildStaticArrayDelegationPlan(
|
|
|
86
88
|
param: elem.param,
|
|
87
89
|
mapPreamble: elem.mapPreamble ?? null,
|
|
88
90
|
offset: elem.offset ?? null,
|
|
91
|
+
indexParam: elem.index ?? null,
|
|
89
92
|
},
|
|
90
93
|
}
|
|
91
94
|
}
|
|
@@ -99,6 +102,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
99
102
|
param: string
|
|
100
103
|
paramBindings: TopLevelLoop['paramBindings']
|
|
101
104
|
key: string | null
|
|
105
|
+
index: string | null
|
|
102
106
|
mapPreamble: string | null
|
|
103
107
|
}): ItemLookup {
|
|
104
108
|
const hasBindings = (args.paramBindings?.length ?? 0) > 0
|
|
@@ -116,6 +120,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
116
120
|
keyWithItem,
|
|
117
121
|
mapPreamble: args.mapPreamble,
|
|
118
122
|
hasBindings,
|
|
123
|
+
indexParam: args.index,
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
return {
|
|
@@ -124,6 +129,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
124
129
|
param: args.param,
|
|
125
130
|
mapPreamble: args.mapPreamble,
|
|
126
131
|
hasBindings,
|
|
132
|
+
indexParam: args.index,
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
|
|
@@ -51,6 +51,12 @@ export interface KeyedItemLookup {
|
|
|
51
51
|
arrayExpr: string
|
|
52
52
|
/** Loop param identifier (or destructure pattern text — used as receiver name only). */
|
|
53
53
|
param: string
|
|
54
|
+
/**
|
|
55
|
+
* Loop index param name (e.g. `i` from `.map((item, i) => ...)`), or `null`.
|
|
56
|
+
* When a delegated handler closes over it, the stringifier re-derives the
|
|
57
|
+
* index at dispatch time and binds it so the reference resolves (#2189).
|
|
58
|
+
*/
|
|
59
|
+
indexParam: string | null
|
|
54
60
|
/** Destructured-binding metadata. Determines TDZ-safe `__bfLoopItem` shape (#951). */
|
|
55
61
|
paramBindings: TopLevelLoop['paramBindings']
|
|
56
62
|
/**
|
|
@@ -72,6 +78,8 @@ export interface DynamicIndexItemLookup {
|
|
|
72
78
|
param: string
|
|
73
79
|
mapPreamble: string | null
|
|
74
80
|
hasBindings: boolean
|
|
81
|
+
/** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
|
|
82
|
+
indexParam: string | null
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
export interface StaticIndexItemLookup {
|
|
@@ -79,6 +87,8 @@ export interface StaticIndexItemLookup {
|
|
|
79
87
|
arrayExpr: string
|
|
80
88
|
param: string
|
|
81
89
|
mapPreamble: string | null
|
|
90
|
+
/** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
|
|
91
|
+
indexParam: string | null
|
|
82
92
|
/**
|
|
83
93
|
* Offset of the loop's items past its preceding container siblings. Its
|
|
84
94
|
* terms are subtracted from the DOM child index to recover the array index,
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { toDomEventName, varSlotId, substituteLoopBindings, buildLoopChildIndexSubtraction, DATA_KEY, keyAttrName } from '../../utils.ts'
|
|
36
|
+
import { extractFreeIdentifiersFromText } from '../../csr-substitute.ts'
|
|
36
37
|
import type {
|
|
37
38
|
EventDelegationPlan,
|
|
38
39
|
KeyedItemLookup,
|
|
@@ -60,6 +61,19 @@ function withTurn(call: string, componentName: string | undefined, childSlotId:
|
|
|
60
61
|
return `beginTurn(${id}); try { ${call} } finally { endTurn() }`
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Bind the loop index under the user's param name for a delegated handler that
|
|
66
|
+
* closes over it (#2189). Returns `null` when there is no index param, the
|
|
67
|
+
* handler doesn't reference it (so unaffected loops keep byte-for-byte output),
|
|
68
|
+
* or the name already equals the in-scope index var (a self-alias). Uses AST
|
|
69
|
+
* free identifiers, not a token match, so `item.id` (property) doesn't count.
|
|
70
|
+
*/
|
|
71
|
+
function indexBindingLine(handler: string, indexParam: string | null, indexExpr: string): string | null {
|
|
72
|
+
if (!indexParam || indexParam === indexExpr) return null
|
|
73
|
+
if (!extractFreeIdentifiersFromText(handler).has(indexParam)) return null
|
|
74
|
+
return `const ${indexParam} = ${indexExpr}`
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
export function stringifyEventDelegation(lines: string[], plan: EventDelegationPlan): void {
|
|
64
78
|
const { containerVar, events, itemLookup, profileComponentName } = plan
|
|
65
79
|
const eventsByName = new Map<string, LoopChildEvent[]>()
|
|
@@ -112,10 +126,11 @@ function emitKeyedLookup(
|
|
|
112
126
|
handlerCall: string,
|
|
113
127
|
lookup: KeyedItemLookup,
|
|
114
128
|
): void {
|
|
115
|
-
const { arrayExpr, param, keyWithItem, mapPreamble, hasBindings } = lookup
|
|
129
|
+
const { arrayExpr, param, keyWithItem, mapPreamble, hasBindings, indexParam } = lookup
|
|
116
130
|
|
|
117
131
|
if (ev.nestedLoops.length === 0) {
|
|
118
132
|
// Single-level keyed lookup.
|
|
133
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, `${arrayExpr}.findIndex(item => String(${keyWithItem}) === key)`)
|
|
119
134
|
ls.push(` const li = ${varSlotId(ev.childSlotId)}El.closest('[${DATA_KEY}]')`)
|
|
120
135
|
ls.push(` if (li) {`)
|
|
121
136
|
ls.push(` const key = li.getAttribute('${DATA_KEY}')`)
|
|
@@ -125,12 +140,14 @@ function emitKeyedLookup(
|
|
|
125
140
|
ls.push(` if (__bfLoopItem) {`)
|
|
126
141
|
ls.push(` const ${param} = __bfLoopItem`)
|
|
127
142
|
if (mapPreamble) ls.push(` ${mapPreamble}`)
|
|
143
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
128
144
|
ls.push(` ${handlerCall}`)
|
|
129
145
|
ls.push(` }`)
|
|
130
146
|
} else {
|
|
131
147
|
ls.push(` const ${param} = ${arrayExpr}.find(item => String(${keyWithItem}) === key)`)
|
|
132
148
|
if (mapPreamble) ls.push(` ${mapPreamble}`)
|
|
133
|
-
ls.push(` if (${param}) ${handlerCall}`)
|
|
149
|
+
if (idxLine) ls.push(` if (${param}) { ${idxLine}; ${handlerCall} }`)
|
|
150
|
+
else ls.push(` if (${param}) ${handlerCall}`)
|
|
134
151
|
}
|
|
135
152
|
ls.push(` }`)
|
|
136
153
|
return
|
|
@@ -164,7 +181,9 @@ function emitKeyedLookup(
|
|
|
164
181
|
const outerGuard = hasBindings ? '__bfLoopItem' : param
|
|
165
182
|
const allParams = [outerGuard, ...ev.nestedLoops.map(n => n.param)]
|
|
166
183
|
if (mapPreamble) ls.push(` ${mapPreamble}`)
|
|
167
|
-
|
|
184
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, `${arrayExpr}.findIndex(item => String(${keyWithItem}) === outerKey)`)
|
|
185
|
+
if (idxLine) ls.push(` if (${allParams.join(' && ')}) { ${idxLine}; ${handlerCall} }`)
|
|
186
|
+
else ls.push(` if (${allParams.join(' && ')}) ${handlerCall}`)
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
function emitDynamicIndexLookup(
|
|
@@ -173,7 +192,8 @@ function emitDynamicIndexLookup(
|
|
|
173
192
|
handlerCall: string,
|
|
174
193
|
lookup: DynamicIndexItemLookup,
|
|
175
194
|
): void {
|
|
176
|
-
const { arrayExpr, param, mapPreamble, hasBindings } = lookup
|
|
195
|
+
const { arrayExpr, param, mapPreamble, hasBindings, indexParam } = lookup
|
|
196
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, 'idx')
|
|
177
197
|
ls.push(` const li = ${varSlotId(ev.childSlotId)}El.closest('li, [bf-i]')`)
|
|
178
198
|
ls.push(` if (li && li.parentElement) {`)
|
|
179
199
|
ls.push(` const idx = Array.from(li.parentElement.children).indexOf(li)`)
|
|
@@ -182,12 +202,14 @@ function emitDynamicIndexLookup(
|
|
|
182
202
|
ls.push(` if (__bfLoopItem) {`)
|
|
183
203
|
ls.push(` const ${param} = __bfLoopItem`)
|
|
184
204
|
if (mapPreamble) ls.push(` ${mapPreamble}`)
|
|
205
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
185
206
|
ls.push(` ${handlerCall}`)
|
|
186
207
|
ls.push(` }`)
|
|
187
208
|
} else {
|
|
188
209
|
ls.push(` const ${param} = ${arrayExpr}[idx]`)
|
|
189
210
|
if (mapPreamble) ls.push(` ${mapPreamble}`)
|
|
190
|
-
ls.push(` if (${param}) ${handlerCall}`)
|
|
211
|
+
if (idxLine) ls.push(` if (${param}) { ${idxLine}; ${handlerCall} }`)
|
|
212
|
+
else ls.push(` if (${param}) ${handlerCall}`)
|
|
191
213
|
}
|
|
192
214
|
ls.push(` }`)
|
|
193
215
|
}
|
|
@@ -199,7 +221,8 @@ function emitStaticIndexLookup(
|
|
|
199
221
|
lookup: StaticIndexItemLookup,
|
|
200
222
|
containerVar: string,
|
|
201
223
|
): void {
|
|
202
|
-
const { arrayExpr, param, mapPreamble, offset } = lookup
|
|
224
|
+
const { arrayExpr, param, mapPreamble, offset, indexParam } = lookup
|
|
225
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, '__idx')
|
|
203
226
|
ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`)
|
|
204
227
|
ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`)
|
|
205
228
|
ls.push(` if (__el.parentElement === ${containerVar}) {`)
|
|
@@ -207,6 +230,7 @@ function emitStaticIndexLookup(
|
|
|
207
230
|
ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`)
|
|
208
231
|
ls.push(` const ${param} = ${arrayExpr}[__idx]`)
|
|
209
232
|
if (mapPreamble) ls.push(` ${mapPreamble}`)
|
|
210
|
-
ls.push(` if (${param}) ${handlerCall}`)
|
|
233
|
+
if (idxLine) ls.push(` if (${param}) { ${idxLine}; ${handlerCall} }`)
|
|
234
|
+
else ls.push(` if (${param}) ${handlerCall}`)
|
|
211
235
|
ls.push(` }`)
|
|
212
236
|
}
|
|
@@ -323,7 +323,7 @@ export function applyPropsRewrite(text: string, propsObjectName: string | null):
|
|
|
323
323
|
return text.replace(new RegExp(`\\b${propsObjectName}\\.`, 'g'), `${PROPS_PARAM}.`)
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
326
|
+
export function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
327
327
|
if (!text || text.trim().length === 0) return new Set()
|
|
328
328
|
const sf = ts.createSourceFile(
|
|
329
329
|
'__free_ids__.ts',
|
|
@@ -10,7 +10,7 @@ import { nameForRegistryRef } from './component-scope.ts'
|
|
|
10
10
|
import { assertNever } from './walker.ts'
|
|
11
11
|
import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } from './csr-substitute.ts'
|
|
12
12
|
import type { ClientJsContext } from './types.ts'
|
|
13
|
-
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE } from '@barefootjs/shared'
|
|
13
|
+
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -145,6 +145,42 @@ function applyIterationShape(
|
|
|
145
145
|
callbackParam: `(${node.param})`,
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
// `objectIteration` (#2168 object-entries-map): reconstruct the STATIC
|
|
149
|
+
// `Object.entries/keys/values(x)` call the compiler stripped at IR-build
|
|
150
|
+
// time (`isObjectIteratorCall`, `jsx-to-ir.ts`) — `arrayExpr` here is just
|
|
151
|
+
// `x` (the plain object), so the client, unlike a template adapter,
|
|
152
|
+
// re-wraps it in real JS to get the actual entries/keys/values array
|
|
153
|
+
// (this runs in a real JS engine, so no per-language lowering is needed).
|
|
154
|
+
//
|
|
155
|
+
// Unlike the array `iterationShape` case, the 'entries' ARRAY wrap does
|
|
156
|
+
// NOT require `node.index` — that field is only populated for a CLEAN
|
|
157
|
+
// 2-identifier destructure (`([word, n]) => …`); an elided/nested
|
|
158
|
+
// pattern (`([, cfg]) => …`) falls through to the generic `paramBindings`
|
|
159
|
+
// machinery instead (`node.param` stays the raw destructure TEXT, e.g.
|
|
160
|
+
// `"[, cfg]"`, which is already a syntactically valid callback param that
|
|
161
|
+
// correctly destructures a `[key, value]` pair — see the trailing
|
|
162
|
+
// fallback below). Only the ARRAY needs wrapping in that case; the
|
|
163
|
+
// callback param is unaffected either way.
|
|
164
|
+
if (node.objectIteration === 'entries') {
|
|
165
|
+
return {
|
|
166
|
+
array: `Object.entries(${arrayExpr})`,
|
|
167
|
+
callbackParam: node.index
|
|
168
|
+
? `([${node.index}, ${node.param}])`
|
|
169
|
+
: `(${node.param}${indexParam})`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (node.objectIteration === 'keys') {
|
|
173
|
+
return {
|
|
174
|
+
array: `Object.keys(${arrayExpr})`,
|
|
175
|
+
callbackParam: `(${node.param})`,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (node.objectIteration === 'values') {
|
|
179
|
+
return {
|
|
180
|
+
array: `Object.values(${arrayExpr})`,
|
|
181
|
+
callbackParam: `(${node.param})`,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
148
184
|
return { array: arrayExpr, callbackParam: `(${node.param}${indexParam})` }
|
|
149
185
|
}
|
|
150
186
|
|
|
@@ -313,7 +349,7 @@ function renderTemplateAttrPart(
|
|
|
313
349
|
case 'boolean-attr':
|
|
314
350
|
return attrName
|
|
315
351
|
case 'literal':
|
|
316
|
-
return `${attrName}="${v.value}"`
|
|
352
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
317
353
|
case 'expression': {
|
|
318
354
|
const valExpr = wrap(v.expr)
|
|
319
355
|
return templateAttrExpr(attrName, valExpr, v.presenceOrUndefined)
|
|
@@ -550,7 +586,9 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
550
586
|
}
|
|
551
587
|
|
|
552
588
|
case 'text':
|
|
553
|
-
|
|
589
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
590
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
591
|
+
return escapeHtml(node.value)
|
|
554
592
|
|
|
555
593
|
case 'expression':
|
|
556
594
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -757,7 +795,7 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
757
795
|
const v = a.value
|
|
758
796
|
switch (v.kind) {
|
|
759
797
|
case 'literal':
|
|
760
|
-
attrParts.push(`${toHtmlAttrName(a.name)}="${v.value}"`)
|
|
798
|
+
attrParts.push(`${toHtmlAttrName(a.name)}="${escapeHtml(v.value)}"`)
|
|
761
799
|
break
|
|
762
800
|
case 'boolean-attr':
|
|
763
801
|
attrParts.push(toHtmlAttrName(a.name))
|
|
@@ -796,7 +834,9 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
796
834
|
}
|
|
797
835
|
|
|
798
836
|
case 'text':
|
|
799
|
-
|
|
837
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
838
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
839
|
+
return escapeHtml(node.value)
|
|
800
840
|
|
|
801
841
|
case 'expression':
|
|
802
842
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -870,7 +910,9 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
870
910
|
}
|
|
871
911
|
|
|
872
912
|
case 'text':
|
|
873
|
-
|
|
913
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
914
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
915
|
+
return escapeHtml(node.value)
|
|
874
916
|
|
|
875
917
|
case 'expression':
|
|
876
918
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1228,7 +1270,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1228
1270
|
return templateAttrExpr(keyName, transformExpr(tmplStr))
|
|
1229
1271
|
}
|
|
1230
1272
|
case 'literal':
|
|
1231
|
-
return `${keyName}="${v.value}"`
|
|
1273
|
+
return `${keyName}="${escapeHtml(v.value)}"`
|
|
1232
1274
|
default:
|
|
1233
1275
|
return ''
|
|
1234
1276
|
}
|
|
@@ -1238,7 +1280,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1238
1280
|
case 'boolean-attr':
|
|
1239
1281
|
return attrName
|
|
1240
1282
|
case 'literal':
|
|
1241
|
-
return `${attrName}="${v.value}"`
|
|
1283
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
1242
1284
|
case 'expression':
|
|
1243
1285
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
|
|
1244
1286
|
case 'template': {
|
|
@@ -1269,7 +1311,9 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1269
1311
|
}
|
|
1270
1312
|
|
|
1271
1313
|
case 'text':
|
|
1272
|
-
|
|
1314
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
1315
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1316
|
+
return escapeHtml(node.value)
|
|
1273
1317
|
|
|
1274
1318
|
case 'expression':
|
|
1275
1319
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1806,7 +1850,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1806
1850
|
case 'boolean-attr':
|
|
1807
1851
|
return attrName
|
|
1808
1852
|
case 'literal':
|
|
1809
|
-
return `${attrName}="${v.value}"`
|
|
1853
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
1810
1854
|
case 'expression':
|
|
1811
1855
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
|
|
1812
1856
|
case 'template': {
|
|
@@ -1837,7 +1881,9 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1837
1881
|
}
|
|
1838
1882
|
|
|
1839
1883
|
case 'text':
|
|
1840
|
-
|
|
1884
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
1885
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1886
|
+
return escapeHtml(node.value)
|
|
1841
1887
|
|
|
1842
1888
|
case 'expression':
|
|
1843
1889
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -283,6 +283,16 @@ export interface LoopCore {
|
|
|
283
283
|
* (#1448 Tier B). Threaded from `IRLoop.iterationShape`.
|
|
284
284
|
*/
|
|
285
285
|
iterationShape?: 'entries' | 'keys'
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Object iteration shape from the STATIC `Object.entries(x)` /
|
|
289
|
+
* `.keys(x)` / `.values(x)` call form (#2168 object-entries-map) —
|
|
290
|
+
* distinct from {@link iterationShape} for the same reason `IRLoop`'s
|
|
291
|
+
* own field is (see that field's docstring, `packages/jsx/src/types.ts`):
|
|
292
|
+
* `x` is a plain object, not an array. Threaded from
|
|
293
|
+
* `IRLoop.objectIteration`.
|
|
294
|
+
*/
|
|
295
|
+
objectIteration?: 'entries' | 'keys' | 'values'
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
/**
|
|
@@ -147,6 +147,38 @@ export function exhaustiveAttrValue(value: never): never {
|
|
|
147
147
|
throw new Error(`Unhandled AttrValue kind: ${JSON.stringify(value)}`)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Reconstruct the `Object.entries/keys/values(x)` call the compiler
|
|
152
|
+
* stripped off at IR-build time (`isObjectIteratorCall`, `jsx-to-ir.ts`)
|
|
153
|
+
* for the CLIENT's array expression — unlike a template adapter, the
|
|
154
|
+
* client runs real JS, so it just re-wraps the plain object `x` (the
|
|
155
|
+
* loop's `array`) to get back the actual iterable `mapArray` needs.
|
|
156
|
+
*
|
|
157
|
+
* Deliberately does NOT also handle the array-only `iterationShape`
|
|
158
|
+
* (`arr.entries()`/`.keys()`) — that shape's `mapArray` callback already
|
|
159
|
+
* gets what it needs from `mapArray`'s own native `(value, index)`
|
|
160
|
+
* signature (the compiler synthesizes `param`/`index` to match those two
|
|
161
|
+
* positions directly), so wrapping the array there would double up. See
|
|
162
|
+
* `IRLoop.objectIteration`'s docstring (`types.ts`) for why the two
|
|
163
|
+
* fields are distinct, and `applyIterationShape` in `html-template.ts`
|
|
164
|
+
* (a different consumer — the CSR template-literal's own inline `.map()`
|
|
165
|
+
* — which does handle both fields, since its shape doesn't reuse
|
|
166
|
+
* `mapArray`'s positional signature).
|
|
167
|
+
*
|
|
168
|
+
* The callback's OWN head (`__bfItem` vs a plain param name) is unrelated
|
|
169
|
+
* and unaffected — that's `destructureLoopParam`'s job
|
|
170
|
+
* (`control-flow/shared.ts`), driven entirely by `param`/`paramBindings`.
|
|
171
|
+
*/
|
|
172
|
+
export function applyObjectIterationWrap(
|
|
173
|
+
node: { objectIteration?: 'entries' | 'keys' | 'values' },
|
|
174
|
+
arrayExpr: string,
|
|
175
|
+
): string {
|
|
176
|
+
if (node.objectIteration === 'entries') return `Object.entries(${arrayExpr})`
|
|
177
|
+
if (node.objectIteration === 'keys') return `Object.keys(${arrayExpr})`
|
|
178
|
+
if (node.objectIteration === 'values') return `Object.values(${arrayExpr})`
|
|
179
|
+
return arrayExpr
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
/**
|
|
151
183
|
* Build the chained array expression for reconcileList. Thin
|
|
152
184
|
* adapter over `buildLoopChainExpr` that unpacks the collected
|
|
@@ -156,12 +188,13 @@ export function exhaustiveAttrValue(value: never): never {
|
|
|
156
188
|
* branch preserves the chain (#1434).
|
|
157
189
|
*/
|
|
158
190
|
export function buildChainedArrayExpr(elem: TopLevelLoop | BranchLoop): string {
|
|
159
|
-
|
|
191
|
+
const chained = buildLoopChainExpr({
|
|
160
192
|
base: elem.array,
|
|
161
193
|
sortComparator: elem.sortComparator,
|
|
162
194
|
filterPredicate: elem.filterPredicate,
|
|
163
195
|
chainOrder: elem.chainOrder,
|
|
164
196
|
})
|
|
197
|
+
return applyObjectIterationWrap(elem, chained)
|
|
165
198
|
}
|
|
166
199
|
|
|
167
200
|
/**
|