@barefootjs/jsx 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/parsed-expr-emitter.d.ts +14 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +137 -0
- 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 +296 -4
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts +4 -0
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +46 -2
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/child-components-in-map.test.ts +84 -0
- package/src/__tests__/expression-parser.test.ts +109 -1
- package/src/__tests__/foreach-client-only.test.ts +80 -0
- package/src/__tests__/ir-reduce-op.test.ts +51 -0
- package/src/__tests__/reduce-op.test.ts +201 -0
- package/src/adapters/parsed-expr-emitter.ts +43 -1
- package/src/expression-parser.ts +570 -4
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +9 -3
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +55 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +47 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +69 -0
package/src/expression-parser.ts
CHANGED
|
@@ -73,6 +73,49 @@ export type ParsedExpr =
|
|
|
73
73
|
args: []
|
|
74
74
|
comparator: SortComparator
|
|
75
75
|
}
|
|
76
|
+
// `.reduce(fn, init)` (#1448 Tier C). Like sort, the reducer is
|
|
77
|
+
// extracted into a structured `ReduceOp` at parse time — the
|
|
78
|
+
// two-param arrow never reaches `args`, so adapters fold via a
|
|
79
|
+
// runtime helper instead of re-walking the callback. The accepted
|
|
80
|
+
// catalogue is the arithmetic-fold family only (`acc + key` /
|
|
81
|
+
// `acc * key`, numeric or string-concat); any other reducer body,
|
|
82
|
+
// or a missing initial value, falls through to `unsupported` so
|
|
83
|
+
// adapters surface BF101 with an @client suggestion. See
|
|
84
|
+
// `extractReduceOpFromTS` below.
|
|
85
|
+
| {
|
|
86
|
+
kind: 'array-method'
|
|
87
|
+
method: 'reduce' | 'reduceRight'
|
|
88
|
+
object: ParsedExpr
|
|
89
|
+
args: []
|
|
90
|
+
reduceOp: ReduceOp
|
|
91
|
+
}
|
|
92
|
+
// `.flat(depth?)` (#1448 Tier C). The flatten depth is validated and
|
|
93
|
+
// normalised into a structured `FlatDepth` at parse time — the literal
|
|
94
|
+
// never reaches `args`, so adapters fold via a runtime helper instead of
|
|
95
|
+
// re-inspecting the depth argument. A non-literal depth refuses with
|
|
96
|
+
// BF101 (the depth must be known at template time). See the `.flat` arm
|
|
97
|
+
// in `convertNode`.
|
|
98
|
+
| {
|
|
99
|
+
kind: 'array-method'
|
|
100
|
+
method: 'flat'
|
|
101
|
+
object: ParsedExpr
|
|
102
|
+
args: []
|
|
103
|
+
flatDepth: FlatDepth
|
|
104
|
+
}
|
|
105
|
+
// `.flatMap(fn)` value-returning field projection (#1448 Tier C). The
|
|
106
|
+
// callback is extracted into a structured `FlatMapOp` (self / field
|
|
107
|
+
// projection) at parse time, mirroring sort / reduce. The projected
|
|
108
|
+
// per-item value is flattened one level (flatMap = map + flat(1)).
|
|
109
|
+
// Array-literal / complex callbacks fall through to `unsupported`; the
|
|
110
|
+
// JSX-returning `.flatMap` is handled as an `IRLoop` upstream and never
|
|
111
|
+
// reaches here. See `extractFlatMapOpFromTS` below.
|
|
112
|
+
| {
|
|
113
|
+
kind: 'array-method'
|
|
114
|
+
method: 'flatMap'
|
|
115
|
+
object: ParsedExpr
|
|
116
|
+
args: []
|
|
117
|
+
flatMapOp: FlatMapOp
|
|
118
|
+
}
|
|
76
119
|
| { kind: 'unsupported'; raw: string; reason: string }
|
|
77
120
|
|
|
78
121
|
/**
|
|
@@ -132,6 +175,95 @@ export type SortComparator = {
|
|
|
132
175
|
method: 'sort' | 'toSorted'
|
|
133
176
|
}
|
|
134
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Structured form of a JS `.reduce((acc, item) => …, init)` call,
|
|
180
|
+
* built once at parse time and consumed by both template adapters'
|
|
181
|
+
* `reduceMethod()` emit (#1448 Tier C). The shape is intentionally
|
|
182
|
+
* finite — only the arithmetic-fold family is lowerable in a
|
|
183
|
+
* declarative template; arbitrary accumulator bodies are not. See
|
|
184
|
+
* `extractReduceOpFromTS` for the accepted catalogue.
|
|
185
|
+
*/
|
|
186
|
+
export type ReduceOp = {
|
|
187
|
+
// The fold operator between accumulator and per-item value. `+`
|
|
188
|
+
// covers numeric sums and (with a string init) string concatenation;
|
|
189
|
+
// `*` covers numeric products. Subtraction / division are excluded —
|
|
190
|
+
// they're order-sensitive and rarely written as a `.reduce`.
|
|
191
|
+
op: '+' | '*'
|
|
192
|
+
// What value each item contributes to the fold:
|
|
193
|
+
// { kind: 'self' } → `acc + item` (primitive array)
|
|
194
|
+
// { kind: 'field', field } → `acc + item.field` (struct-field accessor)
|
|
195
|
+
key: { kind: 'self' } | { kind: 'field'; field: string }
|
|
196
|
+
// Numeric fold vs string concatenation. Determined by the init
|
|
197
|
+
// literal's type: a number init folds numerically; a string init
|
|
198
|
+
// (only valid with `+`) concatenates. Both template runtimes apply
|
|
199
|
+
// the same coercion so their output stays byte-equal; this can
|
|
200
|
+
// diverge from JS for floating-point sums whose decimal expansion
|
|
201
|
+
// differs by runtime (rare in SSR data — integer sums agree).
|
|
202
|
+
type: 'numeric' | 'string'
|
|
203
|
+
// Decoded initial-accumulator value (never raw source). For a numeric
|
|
204
|
+
// fold this is TypeScript's canonical decimal form (`1_000` -> `1000`,
|
|
205
|
+
// `0x10` -> `16`) so `strconv.ParseFloat` / Perl agree; for a concat
|
|
206
|
+
// fold it's the contents of a quoted string literal (only single- or
|
|
207
|
+
// double-quoted `ts.StringLiteral` seeds are accepted — template
|
|
208
|
+
// literals and escape-carrying literals are refused at parse time, so
|
|
209
|
+
// the value is an escape-free single-line string, e.g. the empty
|
|
210
|
+
// string "" or a separator like ", "). Round-trip emitters re-quote a
|
|
211
|
+
// string init via `JSON.stringify`; a numeric init re-emits as-is.
|
|
212
|
+
init: string
|
|
213
|
+
// Original JS source of the reducer body (the returned expression
|
|
214
|
+
// for block bodies). Lets the `@client` fallback ship the user's
|
|
215
|
+
// exact arrow to the JS runtime.
|
|
216
|
+
raw: string
|
|
217
|
+
// The two parameter names the user wrote (e.g. `acc`/`item`, or
|
|
218
|
+
// `sum`/`t`). Only the `@client` fallback reads them — it binds them
|
|
219
|
+
// in a synthetic `(acc, item) => raw` arrow. Server-side lowering
|
|
220
|
+
// works off `op` / `key` / `init` and ignores them.
|
|
221
|
+
paramAcc: string
|
|
222
|
+
paramItem: string
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Flatten depth for `.flat(depth?)` (#1448 Tier C). A finite non-negative
|
|
227
|
+
* integer flattens that many levels (`.flat()` defaults to `1`; a `0` or
|
|
228
|
+
* negative JS depth means "no flatten" → a shallow copy, normalised to
|
|
229
|
+
* `0` here); `'infinity'` is the `.flat(Infinity)` full-depth form. The
|
|
230
|
+
* depth must be a literal — a non-literal argument can't be resolved at
|
|
231
|
+
* template time and refuses with BF101.
|
|
232
|
+
*/
|
|
233
|
+
export type FlatDepth = number | 'infinity'
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A single non-computed projection leaf on the flatMap callback param —
|
|
237
|
+
* the item itself (`i`) or one of its fields (`i.field`). Shared by the
|
|
238
|
+
* scalar and tuple `FlatMapOp` projections.
|
|
239
|
+
*/
|
|
240
|
+
export type FlatMapLeaf = { kind: 'self' } | { kind: 'field'; field: string }
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Structured form of a value-returning `.flatMap(fn)` callback (#1448
|
|
244
|
+
* Tier C). The accepted catalogue:
|
|
245
|
+
*
|
|
246
|
+
* i => i → self projection (flatten one level)
|
|
247
|
+
* i => i.field → field projection (flatten a per-item array field)
|
|
248
|
+
* i => [i.a, i.b] → tuple projection (gather per-item leaves)
|
|
249
|
+
*
|
|
250
|
+
* The scalar `self` / `field` projections return a value that is then
|
|
251
|
+
* flattened one level (flatMap = map + flat(1)) — a non-array value is
|
|
252
|
+
* kept as-is, matching JS. The `tuple` projection returns an array
|
|
253
|
+
* literal; flat(1) removes only that literal's wrapper, so each leaf is
|
|
254
|
+
* appended verbatim (an array-valued leaf is NOT spread). Leaves outside
|
|
255
|
+
* self / field (literals, `i.a + 1`, calls, deep access) refuse with
|
|
256
|
+
* BF101. See `extractFlatMapOpFromTS`.
|
|
257
|
+
*/
|
|
258
|
+
export type FlatMapOp = {
|
|
259
|
+
// What each item projects to before the one-level flatten.
|
|
260
|
+
projection: FlatMapLeaf | { kind: 'tuple'; elements: FlatMapLeaf[] }
|
|
261
|
+
// The callback param name the user wrote (for the `@client` round-trip).
|
|
262
|
+
param: string
|
|
263
|
+
// Original JS source of the callback body (for the `@client` fallback).
|
|
264
|
+
raw: string
|
|
265
|
+
}
|
|
266
|
+
|
|
135
267
|
export type TemplatePart =
|
|
136
268
|
| { type: 'string'; value: string }
|
|
137
269
|
| { type: 'expression'; expr: ParsedExpr }
|
|
@@ -166,6 +298,13 @@ export interface SupportResult {
|
|
|
166
298
|
supported: boolean
|
|
167
299
|
level?: SupportLevel
|
|
168
300
|
reason?: string
|
|
301
|
+
/**
|
|
302
|
+
* The `reason` already spells out the fix (the pre-compute / `@client`
|
|
303
|
+
* hint, or a tailored message like the `forEach` diagnostic), so adapters
|
|
304
|
+
* surface it as-is instead of appending their own remediation block.
|
|
305
|
+
* Low-level reasons (operators, comparators, predicates) leave this unset.
|
|
306
|
+
*/
|
|
307
|
+
selfContained?: boolean
|
|
169
308
|
}
|
|
170
309
|
|
|
171
310
|
// JS Array / String prototype methods that the template-language
|
|
@@ -188,10 +327,30 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
188
327
|
// Higher-order array methods. Seven of these (`filter`, `every`,
|
|
189
328
|
// `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
|
|
190
329
|
// intercepted as `higher-order` IR before reaching this gate;
|
|
191
|
-
// `map` is intercepted as an IRLoop.
|
|
192
|
-
//
|
|
330
|
+
// `map` is intercepted as an IRLoop. `reduce` / `reduceRight` stay
|
|
331
|
+
// listed here so the shapes the Tier C catalogue can't lower still
|
|
332
|
+
// refuse loudly: the `convertNode` call branch intercepts a matching
|
|
333
|
+
// `.reduce(fn, init)` / `.reduceRight(fn, init)` into the structured
|
|
334
|
+
// `array-method` + `ReduceOp` form *before* this gate (returning
|
|
335
|
+
// early), so only the unlowerable fall-throughs (a `.reduce(fn)` with
|
|
336
|
+
// no initial value, or a bare method reference) reach the gate and
|
|
337
|
+
// refuse. A 2-arg call whose reducer/init shape is off-catalogue
|
|
338
|
+
// returns an explicit `unsupported` from the call branch with a richer
|
|
339
|
+
// message. The rest stay refused — see #1448 Tier C for the design
|
|
340
|
+
// questions. `forEach` carries a tailored reason (see
|
|
341
|
+
// `UNSUPPORTED_METHOD_REASONS`).
|
|
342
|
+
// `flat` is no longer here — `.flat(depth?)` lowers via the
|
|
343
|
+
// `array-method` IR (structured `FlatDepth`) + `bf_flat` (Go) /
|
|
344
|
+
// `bf->flat` (Mojo). `flatMap` stays listed as a fallback: the
|
|
345
|
+
// field-projection form (`i => i` / `i => i.field`) lowers via a
|
|
346
|
+
// structured `FlatMapOp`. The convertNode arm intercepts EVERY
|
|
347
|
+
// `.flatMap(...)` call before this gate — matching shapes lower, and the
|
|
348
|
+
// off-catalogue / wrong-arity forms get a tailored `unsupported` reason
|
|
349
|
+
// there — so only a bare method *reference* (`arr.flatMap` uncalled)
|
|
350
|
+
// falls through to this gate. The JSX-returning `.flatMap` lowers as an
|
|
351
|
+
// `IRLoop` upstream. See #1448.
|
|
193
352
|
'filter', 'map', 'reduce', 'reduceRight', 'every', 'some',
|
|
194
|
-
'forEach', 'flatMap',
|
|
353
|
+
'forEach', 'flatMap',
|
|
195
354
|
// #1448 Tier A — Array methods. Each method PR adds the lowering
|
|
196
355
|
// (typically a new `array-method` variant or runtime helper) and
|
|
197
356
|
// removes its row here. See packages/adapter-tests/fixtures/methods/.
|
|
@@ -255,6 +414,20 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
255
414
|
'substring', 'substr', 'match', 'matchAll', 'search',
|
|
256
415
|
])
|
|
257
416
|
|
|
417
|
+
// Per-method override reasons for the BF101 refusal. A method here is still
|
|
418
|
+
// refused via `UNSUPPORTED_METHODS` above; this only swaps the generic hint
|
|
419
|
+
// for a tailored one. Add a row here rather than a branch in the support gate
|
|
420
|
+
// when a method needs special wording.
|
|
421
|
+
const UNSUPPORTED_METHOD_REASONS: Record<string, string> = {
|
|
422
|
+
// `forEach` returns `undefined`, so the generic pre-compute / @client hint
|
|
423
|
+
// is misleading (renders nothing either way) — steer to `.map(...)` /
|
|
424
|
+
// `createEffect`. Rationale pinned in foreach-client-only.test.ts.
|
|
425
|
+
forEach:
|
|
426
|
+
`'.forEach()' returns undefined and has no template-position meaning. ` +
|
|
427
|
+
`Use it for side effects inside an event handler or createEffect callback ` +
|
|
428
|
+
`(client JS), or use '.map(...)' if you meant to render each item.`,
|
|
429
|
+
}
|
|
430
|
+
|
|
258
431
|
// Methods that lower at their single-argument form but whose EXTRA
|
|
259
432
|
// argument is meaningful and NOT yet lowered: the `fromIndex` of
|
|
260
433
|
// `.includes` / `.indexOf` / `.lastIndexOf` (the 2-arg form) and the
|
|
@@ -446,6 +619,45 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
446
619
|
if (callee.property === 'reverse' || callee.property === 'toReversed') {
|
|
447
620
|
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
448
621
|
}
|
|
622
|
+
// `.flat(depth?)` — flatten nested arrays `depth` levels deep
|
|
623
|
+
// (default 1). The depth is validated to a literal and normalised
|
|
624
|
+
// into a structured `FlatDepth` here: `Infinity` becomes the
|
|
625
|
+
// full-depth form, a 0 / negative JS depth normalises to 0 ("no
|
|
626
|
+
// flatten" → shallow copy), and a fractional literal truncates
|
|
627
|
+
// toward zero (JS ToIntegerOrInfinity). A NON-literal depth can't be
|
|
628
|
+
// resolved at template time, so it refuses with BF101 rather than
|
|
629
|
+
// emitting a broken helper call. Go uses `bf_flat`; Mojo uses
|
|
630
|
+
// `bf->flat`. See #1448 Tier C.
|
|
631
|
+
if (callee.property === 'flat') {
|
|
632
|
+
const depthNode = node.arguments[0]
|
|
633
|
+
let flatDepth: FlatDepth
|
|
634
|
+
if (depthNode === undefined) {
|
|
635
|
+
flatDepth = 1
|
|
636
|
+
} else if (ts.isIdentifier(depthNode) && depthNode.text === 'Infinity') {
|
|
637
|
+
flatDepth = 'infinity'
|
|
638
|
+
} else {
|
|
639
|
+
let n: number | undefined
|
|
640
|
+
if (ts.isNumericLiteral(depthNode)) {
|
|
641
|
+
n = Number(depthNode.text)
|
|
642
|
+
} else if (
|
|
643
|
+
ts.isPrefixUnaryExpression(depthNode) &&
|
|
644
|
+
depthNode.operator === ts.SyntaxKind.MinusToken &&
|
|
645
|
+
ts.isNumericLiteral(depthNode.operand)
|
|
646
|
+
) {
|
|
647
|
+
n = -Number(depthNode.operand.text)
|
|
648
|
+
}
|
|
649
|
+
if (n === undefined || Number.isNaN(n)) {
|
|
650
|
+
return {
|
|
651
|
+
kind: 'unsupported',
|
|
652
|
+
raw,
|
|
653
|
+
reason: `\`.flat(depth)\` needs a literal integer or \`Infinity\` depth — a computed depth can't be resolved at template time. Use a literal depth, or pre-compute the value before the template.`,
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const truncated = Math.trunc(n)
|
|
657
|
+
flatDepth = truncated < 0 ? 0 : truncated
|
|
658
|
+
}
|
|
659
|
+
return { kind: 'array-method', method: 'flat', object: callee.object, args: [], flatDepth }
|
|
660
|
+
}
|
|
449
661
|
// `.toLowerCase()` — string-only (the IR carries a value-builtin
|
|
450
662
|
// tag, not a receiver-type discriminator, so the `array-method`
|
|
451
663
|
// label is a misnomer for string methods but the mechanical
|
|
@@ -633,6 +845,85 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
633
845
|
`Wrap the call in /* @client */ to evaluate at hydration.`,
|
|
634
846
|
}
|
|
635
847
|
}
|
|
848
|
+
|
|
849
|
+
// `.reduce(fn, init)` / `.reduceRight(fn, init)` (#1448 Tier C).
|
|
850
|
+
// The reducer + init are extracted into a structured `ReduceOp` at
|
|
851
|
+
// parse time; the two-param arrow never reaches the standard
|
|
852
|
+
// convertNode path (which refuses it), so we read the raw TS AST.
|
|
853
|
+
// Only the arithmetic-fold catalogue lowers — anything else, or a
|
|
854
|
+
// missing init, falls through to `unsupported` (BF101 + @client
|
|
855
|
+
// hint). `reduceRight` shares the catalogue; the method name is
|
|
856
|
+
// preserved so adapters fold right-to-left (only observable for
|
|
857
|
+
// string concatenation — numeric sum / product are commutative).
|
|
858
|
+
if (
|
|
859
|
+
(callee.property === 'reduce' || callee.property === 'reduceRight') &&
|
|
860
|
+
node.arguments.length === 2
|
|
861
|
+
) {
|
|
862
|
+
const reduceOp = extractReduceOpFromTS(node.arguments[0], node.arguments[1])
|
|
863
|
+
if (reduceOp) {
|
|
864
|
+
return {
|
|
865
|
+
kind: 'array-method',
|
|
866
|
+
method: callee.property,
|
|
867
|
+
object: callee.object,
|
|
868
|
+
args: [],
|
|
869
|
+
reduceOp,
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const m = callee.property
|
|
873
|
+
return {
|
|
874
|
+
kind: 'unsupported',
|
|
875
|
+
raw,
|
|
876
|
+
reason:
|
|
877
|
+
`Reduce shape not supported. Accepted (arithmetic fold, explicit init):\n` +
|
|
878
|
+
` arr.${m}((acc, x) => acc + x, 0)\n` +
|
|
879
|
+
` arr.${m}((acc, x) => acc + x.field, 0)\n` +
|
|
880
|
+
` arr.${m}((acc, x) => acc * x.field, 1)\n` +
|
|
881
|
+
` arr.${m}((acc, x) => acc + x.field, '') (string concat)\n` +
|
|
882
|
+
`The accumulator must be the left operand and the initial ` +
|
|
883
|
+
`value a number / string literal. ` +
|
|
884
|
+
`Wrap the call in /* @client */ to evaluate at hydration.`,
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// A `.reduce(fn)` without an initial value can't be lowered: JS
|
|
888
|
+
// throws on an empty array, which a template can't mirror. Fall
|
|
889
|
+
// through to the BF101 gate with the @client escape hatch.
|
|
890
|
+
|
|
891
|
+
// `.flatMap(fn)` value-returning projection (#1448 Tier C). The
|
|
892
|
+
// callback is extracted into a structured `FlatMapOp` (self / field
|
|
893
|
+
// scalar projection, or an array-literal tuple of self / field
|
|
894
|
+
// leaves) from the raw TS AST. The JSX-returning form is handled as
|
|
895
|
+
// an `IRLoop` upstream and never reaches here; richer callbacks
|
|
896
|
+
// refuse with BF101 + the @client hint. Go uses `bf_flat_map` /
|
|
897
|
+
// `bf_flat_map_tuple`; Mojo uses `bf->flat_map` / `bf->flat_map_tuple`.
|
|
898
|
+
// Intercept EVERY `.flatMap(...)` call (not just the 1-arg form) so
|
|
899
|
+
// the off-catalogue and wrong-arity shapes get this tailored reason
|
|
900
|
+
// rather than the generic "flatMap has no template lowering" gate
|
|
901
|
+
// message, which now misleads (the field-projection form does lower).
|
|
902
|
+
if (callee.property === 'flatMap') {
|
|
903
|
+
const flatMapOp =
|
|
904
|
+
node.arguments.length === 1 ? extractFlatMapOpFromTS(node.arguments[0]) : null
|
|
905
|
+
if (flatMapOp) {
|
|
906
|
+
return {
|
|
907
|
+
kind: 'array-method',
|
|
908
|
+
method: 'flatMap',
|
|
909
|
+
object: callee.object,
|
|
910
|
+
args: [],
|
|
911
|
+
flatMapOp,
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
kind: 'unsupported',
|
|
916
|
+
raw,
|
|
917
|
+
reason:
|
|
918
|
+
`flatMap shape not supported. Accepted (self / field leaves, no thisArg):\n` +
|
|
919
|
+
` arr.flatMap(i => i) (flatten one level)\n` +
|
|
920
|
+
` arr.flatMap(i => i.field) (flatten a per-item array field)\n` +
|
|
921
|
+
` arr.flatMap(i => [i.a, i.b]) (gather per-item fields)\n` +
|
|
922
|
+
`Richer callbacks (computed / nested access, arithmetic, calls, ` +
|
|
923
|
+
`literal elements) and the 2-arg \`flatMap(fn, thisArg)\` form ` +
|
|
924
|
+
`aren't lowered. Wrap the call in /* @client */ to evaluate at hydration.`,
|
|
925
|
+
}
|
|
926
|
+
}
|
|
636
927
|
}
|
|
637
928
|
|
|
638
929
|
return { kind: 'call', callee, args }
|
|
@@ -1219,6 +1510,218 @@ function classifySortOperand(
|
|
|
1219
1510
|
return null
|
|
1220
1511
|
}
|
|
1221
1512
|
|
|
1513
|
+
/**
|
|
1514
|
+
* Recover a `ReduceOp` from the `(reducer, init)` args of
|
|
1515
|
+
* `.reduce(...)` (#1448 Tier C). Operates on the raw TS AST because the
|
|
1516
|
+
* standard `convertNode` arrow-fn path rejects two-param arrows.
|
|
1517
|
+
*
|
|
1518
|
+
* The accepted catalogue is intentionally finite — only the
|
|
1519
|
+
* arithmetic-fold family lowers to a declarative template:
|
|
1520
|
+
*
|
|
1521
|
+
* (acc, x) => acc + x → self, numeric (init: number)
|
|
1522
|
+
* (acc, x) => acc + x.field → field, numeric (init: number)
|
|
1523
|
+
* (acc, x) => acc * x → self, numeric (init: number)
|
|
1524
|
+
* (acc, x) => acc * x.field → field, numeric (init: number)
|
|
1525
|
+
* (acc, x) => acc + x → self, string (init: string → concat)
|
|
1526
|
+
* (acc, x) => acc + x.field → field, string (init: string → concat)
|
|
1527
|
+
*
|
|
1528
|
+
* The accumulator must be the binary expression's *left* operand
|
|
1529
|
+
* (canonical reduce form; reversed operands change string-concat
|
|
1530
|
+
* order), the per-item value must be the item param itself or a
|
|
1531
|
+
* single non-computed field access on it, and the init must be a
|
|
1532
|
+
* number or string literal (negative numbers via prefix `-` allowed).
|
|
1533
|
+
* String concatenation requires `+`. Block bodies reduce to a single
|
|
1534
|
+
* `return`, mirroring the sort extractor. Anything else returns null
|
|
1535
|
+
* and the caller emits `unsupported` (BF101).
|
|
1536
|
+
*/
|
|
1537
|
+
export function extractReduceOpFromTS(
|
|
1538
|
+
reducerNode: ts.Node,
|
|
1539
|
+
initNode: ts.Node,
|
|
1540
|
+
): ReduceOp | null {
|
|
1541
|
+
const init = classifyReduceInit(initNode)
|
|
1542
|
+
if (!init) return null
|
|
1543
|
+
|
|
1544
|
+
if (!ts.isArrowFunction(reducerNode) && !ts.isFunctionExpression(reducerNode)) return null
|
|
1545
|
+
// Exactly `(acc, item)` — the index / array reducer params can't be
|
|
1546
|
+
// expressed in a template fold, so refuse the 3- / 4-param forms.
|
|
1547
|
+
if (reducerNode.parameters.length !== 2) return null
|
|
1548
|
+
const pAcc = reducerNode.parameters[0]
|
|
1549
|
+
const pItem = reducerNode.parameters[1]
|
|
1550
|
+
if (!ts.isIdentifier(pAcc.name) || !ts.isIdentifier(pItem.name)) return null
|
|
1551
|
+
const paramAcc = pAcc.name.text
|
|
1552
|
+
const paramItem = pItem.name.text
|
|
1553
|
+
|
|
1554
|
+
// Resolve the reducer body: expression-bodied arrow directly; block
|
|
1555
|
+
// bodies (arrow `=> { … }` and function expressions) must reduce to
|
|
1556
|
+
// exactly one `return <expr>;` — mirrors `extractSortComparatorFromTS`.
|
|
1557
|
+
let body: ts.Expression
|
|
1558
|
+
if (ts.isArrowFunction(reducerNode) && !ts.isBlock(reducerNode.body)) {
|
|
1559
|
+
body = reducerNode.body
|
|
1560
|
+
} else {
|
|
1561
|
+
const block = reducerNode.body as ts.Block
|
|
1562
|
+
const stmts = block.statements
|
|
1563
|
+
if (stmts.length !== 1 || !ts.isReturnStatement(stmts[0]) || !stmts[0].expression) return null
|
|
1564
|
+
body = stmts[0].expression
|
|
1565
|
+
}
|
|
1566
|
+
const raw = body.getText()
|
|
1567
|
+
|
|
1568
|
+
const expr = unwrapParens(body)
|
|
1569
|
+
if (!ts.isBinaryExpression(expr)) return null
|
|
1570
|
+
let op: '+' | '*'
|
|
1571
|
+
if (expr.operatorToken.kind === ts.SyntaxKind.PlusToken) op = '+'
|
|
1572
|
+
else if (expr.operatorToken.kind === ts.SyntaxKind.AsteriskToken) op = '*'
|
|
1573
|
+
else return null
|
|
1574
|
+
|
|
1575
|
+
// The accumulator must be the left operand (`acc + x`, not `x + acc`).
|
|
1576
|
+
const left = unwrapParens(expr.left)
|
|
1577
|
+
if (!ts.isIdentifier(left) || left.text !== paramAcc) return null
|
|
1578
|
+
|
|
1579
|
+
const key = classifyReduceKey(unwrapParens(expr.right), paramItem)
|
|
1580
|
+
if (!key) return null
|
|
1581
|
+
|
|
1582
|
+
// String concatenation only makes sense with `+`.
|
|
1583
|
+
const type: 'numeric' | 'string' = init.type
|
|
1584
|
+
if (type === 'string' && op !== '+') return null
|
|
1585
|
+
|
|
1586
|
+
return { op, key, type, init: init.value, raw, paramAcc, paramItem }
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Recover a `FlatMapOp` from the single-argument callback of a
|
|
1591
|
+
* value-returning `.flatMap(fn)` (#1448 Tier C). Operates on the raw TS
|
|
1592
|
+
* AST, mirroring `extractReduceOpFromTS`.
|
|
1593
|
+
*
|
|
1594
|
+
* The accepted catalogue:
|
|
1595
|
+
*
|
|
1596
|
+
* i => i → self (flatMap(identity) === flat(1))
|
|
1597
|
+
* i => i.field → field (flatten a per-item array field)
|
|
1598
|
+
* i => [i.a, i.b] → tuple (gather per-item self / field leaves)
|
|
1599
|
+
*
|
|
1600
|
+
* The callback must take exactly one identifier param (the index / array
|
|
1601
|
+
* params can't be expressed in a template projection), and the body must
|
|
1602
|
+
* be the param itself, a single non-computed field access on it, or an
|
|
1603
|
+
* array literal whose every element is one of those leaves. Block bodies
|
|
1604
|
+
* reduce to a single `return`, like the reduce / sort extractors. Any
|
|
1605
|
+
* other body (deep access, computed members, calls, arithmetic, a
|
|
1606
|
+
* literal element) returns null and the caller emits `unsupported`
|
|
1607
|
+
* (BF101).
|
|
1608
|
+
*/
|
|
1609
|
+
export function extractFlatMapOpFromTS(cbNode: ts.Node): FlatMapOp | null {
|
|
1610
|
+
if (!ts.isArrowFunction(cbNode) && !ts.isFunctionExpression(cbNode)) return null
|
|
1611
|
+
// Exactly `(item)` — a `(item, index)` / `(item, index, array)` callback
|
|
1612
|
+
// can't be lowered to a declarative projection.
|
|
1613
|
+
if (cbNode.parameters.length !== 1) return null
|
|
1614
|
+
const p = cbNode.parameters[0]
|
|
1615
|
+
if (!ts.isIdentifier(p.name)) return null
|
|
1616
|
+
const param = p.name.text
|
|
1617
|
+
|
|
1618
|
+
let body: ts.Expression
|
|
1619
|
+
if (ts.isArrowFunction(cbNode) && !ts.isBlock(cbNode.body)) {
|
|
1620
|
+
body = cbNode.body
|
|
1621
|
+
} else {
|
|
1622
|
+
const block = cbNode.body as ts.Block
|
|
1623
|
+
const stmts = block.statements
|
|
1624
|
+
if (stmts.length !== 1 || !ts.isReturnStatement(stmts[0]) || !stmts[0].expression) return null
|
|
1625
|
+
body = stmts[0].expression
|
|
1626
|
+
}
|
|
1627
|
+
const raw = body.getText()
|
|
1628
|
+
const inner = unwrapParens(body)
|
|
1629
|
+
|
|
1630
|
+
// Array-literal body → tuple projection. Every element must be a
|
|
1631
|
+
// self / field leaf; a literal / computed / nested element refuses the
|
|
1632
|
+
// whole shape (the per-item evaluation of richer expressions isn't
|
|
1633
|
+
// lowered). flat(1) removes only the literal's wrapper, so each leaf is
|
|
1634
|
+
// appended verbatim — handled by the `bf_flat_map_tuple` runtime.
|
|
1635
|
+
if (ts.isArrayLiteralExpression(inner)) {
|
|
1636
|
+
// An empty tuple (`i => []`) is a degenerate no-op projection (always
|
|
1637
|
+
// yields nothing). Refuse it so the emitters never produce a
|
|
1638
|
+
// zero-arg `bf_flat_map_tuple` / `bf->flat_map_tuple(...,)` call.
|
|
1639
|
+
if (inner.elements.length === 0) return null
|
|
1640
|
+
const elements: FlatMapLeaf[] = []
|
|
1641
|
+
for (const el of inner.elements) {
|
|
1642
|
+
// Spread / holes (`[...xs]`, `[, x]`) aren't leaves.
|
|
1643
|
+
if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) return null
|
|
1644
|
+
const leaf = classifyReduceKey(unwrapParens(el), param)
|
|
1645
|
+
if (!leaf) return null
|
|
1646
|
+
elements.push(leaf)
|
|
1647
|
+
}
|
|
1648
|
+
return { projection: { kind: 'tuple', elements }, param, raw }
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Scalar body. Reuse the reduce key classifier — `i` → self,
|
|
1652
|
+
// `i.field` → field, null for anything deeper (`i.a.b`, `i[k]`, a call).
|
|
1653
|
+
const leaf = classifyReduceKey(inner, param)
|
|
1654
|
+
if (!leaf) return null
|
|
1655
|
+
|
|
1656
|
+
return { projection: leaf, param, raw }
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Classify a reduce per-item operand into a `ReduceOp` key. Accepts
|
|
1661
|
+
* the bare item param (`x` → self) and a single non-computed field
|
|
1662
|
+
* access (`x.field` → field); returns null for anything deeper
|
|
1663
|
+
* (`x.a.b`, `x[k]`, a literal, a call, …).
|
|
1664
|
+
*/
|
|
1665
|
+
function classifyReduceKey(
|
|
1666
|
+
expr: ts.Expression,
|
|
1667
|
+
paramItem: string,
|
|
1668
|
+
): { kind: 'self' } | { kind: 'field'; field: string } | null {
|
|
1669
|
+
if (ts.isIdentifier(expr)) {
|
|
1670
|
+
return expr.text === paramItem ? { kind: 'self' } : null
|
|
1671
|
+
}
|
|
1672
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
1673
|
+
if (expr.expression.text === paramItem) return { kind: 'field', field: expr.name.text }
|
|
1674
|
+
}
|
|
1675
|
+
return null
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Classify a reduce initial-value node into the *decoded* fold seed.
|
|
1680
|
+
* Accepts a numeric literal (optionally prefixed with `-`) and a string
|
|
1681
|
+
* literal; returns `{ type, value }` where `value` is the canonical
|
|
1682
|
+
* value — never the raw source text. Any other init (a variable, a
|
|
1683
|
+
* call, an object) returns null — the fold start value must be
|
|
1684
|
+
* statically known.
|
|
1685
|
+
*
|
|
1686
|
+
* Numeric: `node.text` is TypeScript's canonical decimal form, so
|
|
1687
|
+
* separators and non-decimal radices fold uniformly across adapters
|
|
1688
|
+
* (`1_000` → `1000`, `0x10` → `16`, `1e3` → `1000`). The Go runtime's
|
|
1689
|
+
* `strconv.ParseFloat` and Perl both accept that decimal string —
|
|
1690
|
+
* passing the raw source (`0x10`, `1_000`) would silently fold to 0 on
|
|
1691
|
+
* Go while Perl accepted it (#1728 review).
|
|
1692
|
+
*
|
|
1693
|
+
* String: `node.text` is the *unescaped* contents. To keep the three
|
|
1694
|
+
* adapters byte-equal without teaching each one to re-decode JS escapes
|
|
1695
|
+
* (`\n`, `\u{…}`, `\\`, an escaped quote), we refuse any string literal
|
|
1696
|
+
* whose contents differ from its raw inner source — i.e. any literal
|
|
1697
|
+
* carrying an escape sequence. Accepted seeds are therefore escape-free
|
|
1698
|
+
* single-line strings (`''`, `', '`, `'-'`), which embed safely in both
|
|
1699
|
+
* the Go-template `"…"` operand and the Perl single-quoted literal. The
|
|
1700
|
+
* realistic concat seed (`''`) is unaffected; richer seeds fall back to
|
|
1701
|
+
* the `@client` escape hatch.
|
|
1702
|
+
*/
|
|
1703
|
+
function classifyReduceInit(
|
|
1704
|
+
node: ts.Node,
|
|
1705
|
+
): { type: 'numeric' | 'string'; value: string } | null {
|
|
1706
|
+
// Unwrap redundant parens (`(0)` / `(-1)`) so they classify like the
|
|
1707
|
+
// bare literal — matches the extractor's `unwrapParens` use elsewhere.
|
|
1708
|
+
let n: ts.Node = unwrapParens(node as ts.Expression)
|
|
1709
|
+
// `-1` parses as a prefix-minus over a numeric literal.
|
|
1710
|
+
if (ts.isPrefixUnaryExpression(n) && n.operator === ts.SyntaxKind.MinusToken) {
|
|
1711
|
+
if (ts.isNumericLiteral(n.operand)) return { type: 'numeric', value: '-' + n.operand.text }
|
|
1712
|
+
return null
|
|
1713
|
+
}
|
|
1714
|
+
if (ts.isNumericLiteral(n)) return { type: 'numeric', value: n.text }
|
|
1715
|
+
if (ts.isStringLiteral(n)) {
|
|
1716
|
+
// Refuse literals carrying escapes so the decoded value equals its
|
|
1717
|
+
// raw inner source (and thus embeds safely + byte-equal everywhere).
|
|
1718
|
+
const raw = n.getText()
|
|
1719
|
+
if (raw.length < 2 || raw.slice(1, -1) !== n.text) return null
|
|
1720
|
+
return { type: 'string', value: n.text }
|
|
1721
|
+
}
|
|
1722
|
+
return null
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1222
1725
|
/**
|
|
1223
1726
|
* Per-binding entry stored in `fieldMap`: the dotted path from the
|
|
1224
1727
|
* synthetic param down to the field, plus an optional default
|
|
@@ -1767,6 +2270,22 @@ function substituteDestructuredFields(
|
|
|
1767
2270
|
// enclosing destructure). Preserve verbatim.
|
|
1768
2271
|
return { kind: 'array-method', method: e.method, object: walk(e.object), args: [], comparator: e.comparator }
|
|
1769
2272
|
}
|
|
2273
|
+
if (e.method === 'reduce' || e.method === 'reduceRight') {
|
|
2274
|
+
// `ReduceOp` is a structured value referencing its own
|
|
2275
|
+
// paramAcc / paramItem, never the enclosing destructure —
|
|
2276
|
+
// preserve verbatim, same as the sort comparator above.
|
|
2277
|
+
return { kind: 'array-method', method: e.method, object: walk(e.object), args: [], reduceOp: e.reduceOp }
|
|
2278
|
+
}
|
|
2279
|
+
if (e.method === 'flat') {
|
|
2280
|
+
// `flatDepth` is a normalised literal — no destructure refs to
|
|
2281
|
+
// substitute. Preserve verbatim, same as sort / reduce above.
|
|
2282
|
+
return { kind: 'array-method', method: 'flat', object: walk(e.object), args: [], flatDepth: e.flatDepth }
|
|
2283
|
+
}
|
|
2284
|
+
if (e.method === 'flatMap') {
|
|
2285
|
+
// `FlatMapOp` references its own callback param, never the
|
|
2286
|
+
// enclosing destructure — preserve verbatim, like reduce / sort.
|
|
2287
|
+
return { kind: 'array-method', method: 'flatMap', object: walk(e.object), args: [], flatMapOp: e.flatMapOp }
|
|
2288
|
+
}
|
|
1770
2289
|
return { kind: 'array-method', method: e.method, object: walk(e.object), args: e.args.map(walk) }
|
|
1771
2290
|
case 'literal':
|
|
1772
2291
|
case 'unsupported':
|
|
@@ -1918,11 +2437,17 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
1918
2437
|
// This handles the case where the pattern wasn't recognized as higher-order
|
|
1919
2438
|
if (expr.callee.kind === 'member') {
|
|
1920
2439
|
const methodName = expr.callee.property
|
|
2440
|
+
// No template lowering → BF101. `UNSUPPORTED_METHOD_REASONS` supplies
|
|
2441
|
+
// a tailored reason for some methods (e.g. `forEach`); the rest get the
|
|
2442
|
+
// generic hint. Both already carry next steps, hence `selfContained`.
|
|
1921
2443
|
if (UNSUPPORTED_METHODS.has(methodName)) {
|
|
1922
2444
|
return {
|
|
1923
2445
|
supported: false,
|
|
1924
2446
|
level: 'L5_UNSUPPORTED',
|
|
1925
|
-
|
|
2447
|
+
selfContained: true,
|
|
2448
|
+
reason:
|
|
2449
|
+
UNSUPPORTED_METHOD_REASONS[methodName] ??
|
|
2450
|
+
`'${methodName}()' can't render on the server. Pre-compute the value, or add /* @client */ for client-only (no SSR).`,
|
|
1926
2451
|
}
|
|
1927
2452
|
}
|
|
1928
2453
|
}
|
|
@@ -2233,6 +2758,24 @@ export function exprToString(expr: ParsedExpr): string {
|
|
|
2233
2758
|
const { paramA, paramB, raw } = expr.comparator
|
|
2234
2759
|
return `${exprToString(expr.object)}.${expr.method}((${paramA},${paramB}) => ${raw})`
|
|
2235
2760
|
}
|
|
2761
|
+
if (expr.method === 'reduce' || expr.method === 'reduceRight') {
|
|
2762
|
+
const { paramAcc, paramItem, raw, type, init } = expr.reduceOp
|
|
2763
|
+
// `init` is the decoded value: re-quote a string seed, re-emit a
|
|
2764
|
+
// numeric seed as-is (it's already a valid number literal).
|
|
2765
|
+
const initSrc = type === 'string' ? JSON.stringify(init) : init
|
|
2766
|
+
return `${exprToString(expr.object)}.${expr.method}((${paramAcc},${paramItem}) => ${raw}, ${initSrc})`
|
|
2767
|
+
}
|
|
2768
|
+
if (expr.method === 'flat') {
|
|
2769
|
+
// Preserve the normalised depth so diagnostics don't misleadingly
|
|
2770
|
+
// print `.flat()` for a `.flat(2)` / `.flat(Infinity)` source.
|
|
2771
|
+
const d = expr.flatDepth
|
|
2772
|
+
const depthSrc = d === 'infinity' ? 'Infinity' : String(d)
|
|
2773
|
+
return `${exprToString(expr.object)}.flat(${d === 1 ? '' : depthSrc})`
|
|
2774
|
+
}
|
|
2775
|
+
if (expr.method === 'flatMap') {
|
|
2776
|
+
const { param, raw } = expr.flatMapOp
|
|
2777
|
+
return `${exprToString(expr.object)}.flatMap(${param} => ${raw})`
|
|
2778
|
+
}
|
|
2236
2779
|
return `${exprToString(expr.object)}.${expr.method}(${expr.args.map(exprToString).join(', ')})`
|
|
2237
2780
|
case 'unsupported':
|
|
2238
2781
|
return `[UNSUPPORTED: ${expr.raw}]`
|
|
@@ -2295,6 +2838,29 @@ export function stringifyParsedExpr(expr: ParsedExpr): string {
|
|
|
2295
2838
|
const { paramA, paramB, raw } = expr.comparator
|
|
2296
2839
|
return `${stringifyParsedExpr(expr.object)}.${expr.method}((${paramA},${paramB}) => ${raw})`
|
|
2297
2840
|
}
|
|
2841
|
+
if (expr.method === 'reduce' || expr.method === 'reduceRight') {
|
|
2842
|
+
// Round-trip the user's param names + init so downstream
|
|
2843
|
+
// re-parsers (the CSR / Hono JS path, templatePrimitive
|
|
2844
|
+
// substitution) see valid JS — `raw` references the names
|
|
2845
|
+
// verbatim. `init` is the decoded value: re-quote a string
|
|
2846
|
+
// seed via JSON.stringify, re-emit a numeric seed as-is.
|
|
2847
|
+
const { paramAcc, paramItem, raw, type, init } = expr.reduceOp
|
|
2848
|
+
const initSrc = type === 'string' ? JSON.stringify(init) : init
|
|
2849
|
+
return `${stringifyParsedExpr(expr.object)}.${expr.method}((${paramAcc},${paramItem}) => ${raw}, ${initSrc})`
|
|
2850
|
+
}
|
|
2851
|
+
if (expr.method === 'flat') {
|
|
2852
|
+
// Round-trip the normalised depth back to JS for the CSR / Hono
|
|
2853
|
+
// path: `'infinity'` → `Infinity`, `1` is left implicit (`.flat()`).
|
|
2854
|
+
const d = expr.flatDepth
|
|
2855
|
+
const depthSrc = d === 'infinity' ? 'Infinity' : String(d)
|
|
2856
|
+
return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? '' : depthSrc})`
|
|
2857
|
+
}
|
|
2858
|
+
if (expr.method === 'flatMap') {
|
|
2859
|
+
// Round-trip the user's callback param + body so the CSR / Hono
|
|
2860
|
+
// path re-parses valid JS (`raw` references the param verbatim).
|
|
2861
|
+
const { param, raw } = expr.flatMapOp
|
|
2862
|
+
return `${stringifyParsedExpr(expr.object)}.flatMap(${param} => ${raw})`
|
|
2863
|
+
}
|
|
2298
2864
|
return `${stringifyParsedExpr(expr.object)}.${expr.method}(${expr.args.map(stringifyParsedExpr).join(', ')})`
|
|
2299
2865
|
case 'unsupported':
|
|
2300
2866
|
return expr.raw
|
package/src/index.ts
CHANGED
|
@@ -245,7 +245,7 @@ export { ErrorCodes, createError, formatError, generateCodeFrame } from './error
|
|
|
245
245
|
|
|
246
246
|
// Expression Parser
|
|
247
247
|
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder } from './expression-parser'
|
|
248
|
-
export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, SupportLevel, SupportResult, TemplatePart } from './expression-parser'
|
|
248
|
+
export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, ReduceOp, FlatDepth, FlatMapOp, FlatMapLeaf, SupportLevel, SupportResult, TemplatePart } from './expression-parser'
|
|
249
249
|
export { buildLoopChainExpr } from './loop-chain'
|
|
250
250
|
export type { LoopChainInputs } from './loop-chain'
|
|
251
251
|
|