@barefootjs/jsx 0.18.3 → 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 +41 -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.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -48
- 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__/__snapshots__/doc-examples.test.ts.snap +1 -1
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/ir-const-resolution.test.ts +1 -1
- package/src/__tests__/ir-provider.test.ts +2 -2
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/tagged-template-interleave.test.ts +2 -2
- package/src/adapters/parsed-expr-emitter.ts +76 -1
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +1 -1
- 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 +110 -13
- package/src/types.ts +68 -0
|
@@ -67,11 +67,14 @@ export type ArrayMethod =
|
|
|
67
67
|
| 'toLowerCase'
|
|
68
68
|
| 'toUpperCase'
|
|
69
69
|
| 'trim'
|
|
70
|
+
| 'trimStart'
|
|
71
|
+
| 'trimEnd'
|
|
70
72
|
| 'toFixed'
|
|
71
73
|
| 'split'
|
|
72
74
|
| 'startsWith'
|
|
73
75
|
| 'endsWith'
|
|
74
76
|
| 'replace'
|
|
77
|
+
| 'replaceAll'
|
|
75
78
|
| 'repeat'
|
|
76
79
|
| 'padStart'
|
|
77
80
|
| 'padEnd'
|
|
@@ -111,10 +114,16 @@ export interface ParsedExprEmitter {
|
|
|
111
114
|
identifier(name: string): string
|
|
112
115
|
literal(value: string | number | boolean | null, literalType: LiteralType): string
|
|
113
116
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string
|
|
117
|
+
// `optional` is true for a `?.`-written access (`user?.name`); see the
|
|
118
|
+
// `ParsedExpr` `member` variant's docstring in `expression-parser.ts`
|
|
119
|
+
// for the single-hop caveat. Every adapter's `member()` implementation
|
|
120
|
+
// that doesn't need it (its lowering is already null-safe) is free to
|
|
121
|
+
// ignore the parameter.
|
|
114
122
|
member(
|
|
115
123
|
object: ParsedExpr,
|
|
116
124
|
property: string,
|
|
117
125
|
computed: boolean,
|
|
126
|
+
optional: boolean,
|
|
118
127
|
emit: (e: ParsedExpr) => string,
|
|
119
128
|
): string
|
|
120
129
|
// Element access with a non-literal index (`arr[index]`). The index
|
|
@@ -198,6 +207,72 @@ export interface ParsedExprEmitter {
|
|
|
198
207
|
unsupported(raw: string, reason: string): string
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Whether an operand is string-typed, as far as the ParsedExpr tree can
|
|
212
|
+
* tell: a string literal, a template literal, a zero-arg getter call /
|
|
213
|
+
* `props.x` member whose name the adapter knows to be string-valued
|
|
214
|
+
* (`isStringName`, from adapter state), or a `+` chain that is itself a
|
|
215
|
+
* string concatenation. Promoted from the Mojo/Xslate adapters' local
|
|
216
|
+
* copies (their file header marked it a shared candidate) and extended
|
|
217
|
+
* with the template-literal and nested-`+` arms.
|
|
218
|
+
*
|
|
219
|
+
* Consumed by `===`/`!==` lowering on backends whose `==` is numeric
|
|
220
|
+
* (Perl `eq`/`ne`) and by `isStringConcatBinary` below.
|
|
221
|
+
*/
|
|
222
|
+
export function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
|
|
223
|
+
if (expr.kind === 'literal' && expr.literalType === 'string') return true
|
|
224
|
+
if (expr.kind === 'template-literal') return true
|
|
225
|
+
if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
226
|
+
return isStringName(expr.callee.name)
|
|
227
|
+
}
|
|
228
|
+
if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
|
|
229
|
+
return isStringName(expr.property)
|
|
230
|
+
}
|
|
231
|
+
if (expr.kind === 'binary' && expr.op === '+') {
|
|
232
|
+
return isStringTypedOperand(expr.left, isStringName) || isStringTypedOperand(expr.right, isStringName)
|
|
233
|
+
}
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Whether a `binary` node is JS STRING concatenation rather than numeric
|
|
239
|
+
* addition: `+` with at least one string-typed operand (#2176). JS `+`
|
|
240
|
+
* overloads on operand type; backends whose `+` is numeric-only coerce
|
|
241
|
+
* the strings — Perl renders `'Hello, ' + name` as 0, PHP fatals with
|
|
242
|
+
* "Unsupported operand types" — so their emitters must pick the
|
|
243
|
+
* language's concat operator (`.` / `~`) when this returns true. The
|
|
244
|
+
* decision is shared-layer semantics; each adapter only maps true to
|
|
245
|
+
* its own operator.
|
|
246
|
+
*/
|
|
247
|
+
export function isStringConcatBinary(
|
|
248
|
+
op: string,
|
|
249
|
+
left: ParsedExpr,
|
|
250
|
+
right: ParsedExpr,
|
|
251
|
+
isStringName: (n: string) => boolean,
|
|
252
|
+
): boolean {
|
|
253
|
+
return op === '+' && (isStringTypedOperand(left, isStringName) || isStringTypedOperand(right, isStringName))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Wrap an emitted binary/logical/ternary OPERAND in parentheses so the
|
|
258
|
+
* source grouping the `ParsedExpr` tree encodes survives infix
|
|
259
|
+
* re-emission (#2173). `(count() + 2) * 3` parses as
|
|
260
|
+
* `binary{*, binary{+}, 3}` — the tree is unambiguous, but an emitter
|
|
261
|
+
* that joins operands textually (`${l} ${op} ${r}`) re-exposes the
|
|
262
|
+
* text to the TARGET language's precedence, silently computing
|
|
263
|
+
* `count + 2 * 3`. Grouping is decided here, in the shared layer
|
|
264
|
+
* (the semantics), so adapters just call this on each operand — no
|
|
265
|
+
* per-language precedence table needed: parenthesizing a compound
|
|
266
|
+
* operand is universally valid, and leaf operands (identifiers,
|
|
267
|
+
* literals, calls, members) stay unwrapped so simple emissions remain
|
|
268
|
+
* byte-identical.
|
|
269
|
+
*/
|
|
270
|
+
export function groupBinaryOperand(operand: ParsedExpr, emitted: string): string {
|
|
271
|
+
return operand.kind === 'binary' || operand.kind === 'logical' || operand.kind === 'conditional'
|
|
272
|
+
? `(${emitted})`
|
|
273
|
+
: emitted
|
|
274
|
+
}
|
|
275
|
+
|
|
201
276
|
/**
|
|
202
277
|
* Single point of dispatch from `ParsedExpr.kind` to the adapter's
|
|
203
278
|
* method. Adapters call this once at their entry point; the recursion
|
|
@@ -222,7 +297,7 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
222
297
|
return emitter.call(expr.callee, expr.args, emit)
|
|
223
298
|
}
|
|
224
299
|
case 'member':
|
|
225
|
-
return emitter.member(expr.object, expr.property, expr.computed, emit)
|
|
300
|
+
return emitter.member(expr.object, expr.property, expr.computed, expr.optional, emit)
|
|
226
301
|
case 'index-access':
|
|
227
302
|
return emitter.indexAccess(expr.object, expr.index, emit)
|
|
228
303
|
case 'binary':
|
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':
|
package/src/index.ts
CHANGED
|
@@ -85,7 +85,7 @@ export type {
|
|
|
85
85
|
export { JsxAdapter } from './adapters/jsx-adapter.ts'
|
|
86
86
|
export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
87
87
|
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
88
|
-
export { emitParsedExpr } from './adapters/parsed-expr-emitter.ts'
|
|
88
|
+
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
89
89
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
90
90
|
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
91
91
|
export type { EnvSignalReader } from './adapters/env-signal.ts'
|
|
@@ -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',
|