@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/jsx-to-ir.ts
CHANGED
|
@@ -48,7 +48,7 @@ import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironme
|
|
|
48
48
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
49
49
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
50
50
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
51
|
-
import { toHTMLAttrName } from '@barefootjs/shared'
|
|
51
|
+
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
52
52
|
|
|
53
53
|
// =============================================================================
|
|
54
54
|
// Transform Context
|
|
@@ -82,6 +82,17 @@ interface TransformContext {
|
|
|
82
82
|
_destructuredPropNames?: Set<string> | null
|
|
83
83
|
/** Active loop parameter names for slotId assignment to loop-param-dependent expressions */
|
|
84
84
|
loopParams: Set<string>
|
|
85
|
+
/**
|
|
86
|
+
* Count of enclosing `.map()` loops (0 = outermost), incremented/
|
|
87
|
+
* decremented in lockstep with entering/leaving `transformMapCall`.
|
|
88
|
+
* Unlike `loopParams` (a name Set that can gain several entries for
|
|
89
|
+
* ONE loop level via destructuring), this is a plain per-level
|
|
90
|
+
* counter — the single source of truth `IRLoop.depth` is stamped
|
|
91
|
+
* from, so every adapter's `data-key`/`data-key-N` suffix derives
|
|
92
|
+
* from one IR-computed value instead of each adapter re-deriving
|
|
93
|
+
* nesting depth its own way (#2168 nested-loop-outer-binding).
|
|
94
|
+
*/
|
|
95
|
+
loopDepth: number
|
|
85
96
|
/** Counter for async boundary IDs (a0, a1, ...) */
|
|
86
97
|
asyncIdCounter: number
|
|
87
98
|
/** Counter for <Region> structural index (0, 1, ...) within a file. */
|
|
@@ -395,6 +406,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
395
406
|
isRoot: true,
|
|
396
407
|
insideComponentChildren: false,
|
|
397
408
|
loopParams: new Set(),
|
|
409
|
+
loopDepth: 0,
|
|
398
410
|
patterns: {
|
|
399
411
|
signals: analyzer.signals.map(s => ({
|
|
400
412
|
getter: s.getter,
|
|
@@ -1563,7 +1575,12 @@ function transformText(node: ts.JsxText, ctx: TransformContext): IRText | null {
|
|
|
1563
1575
|
|
|
1564
1576
|
return {
|
|
1565
1577
|
type: 'text',
|
|
1566
|
-
|
|
1578
|
+
// JSX decodes character references at parse time (`©` IS the
|
|
1579
|
+
// text `©`), so the IR carries the DECODED value — the semantics —
|
|
1580
|
+
// and each adapter re-escapes for its own emission context.
|
|
1581
|
+
// Decode AFTER whitespace normalization: ` ` yields U+00A0,
|
|
1582
|
+
// which `\s+` would otherwise collapse into a plain space.
|
|
1583
|
+
value: decodeEntities(text),
|
|
1567
1584
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1568
1585
|
}
|
|
1569
1586
|
}
|
|
@@ -2483,6 +2500,31 @@ function isIteratorShapeCall(
|
|
|
2483
2500
|
return { array: node.expression.expression, shape: name }
|
|
2484
2501
|
}
|
|
2485
2502
|
|
|
2503
|
+
/**
|
|
2504
|
+
* Check if a node is the STATIC `Object.entries(x)` / `Object.keys(x)` /
|
|
2505
|
+
* `Object.values(x)` call form (#2168 object-entries-map) — the
|
|
2506
|
+
* one-argument form where `x` is a plain object/Record being iterated,
|
|
2507
|
+
* as opposed to {@link isIteratorShapeCall}'s zero-arg instance-method
|
|
2508
|
+
* form (`arr.entries()`) on an actual array. Returns the object
|
|
2509
|
+
* expression (any expression — `props.x`, `x ?? {}`, not just a bare
|
|
2510
|
+
* identifier) and the iteration shape so `transformMapCall` can strip
|
|
2511
|
+
* the `Object.<method>(...)` wrapper and record it on the IRLoop as
|
|
2512
|
+
* `objectIteration` (see that field's docstring in `types.ts` for why
|
|
2513
|
+
* this is a distinct field from `iterationShape`, not a shared one).
|
|
2514
|
+
*/
|
|
2515
|
+
function isObjectIteratorCall(
|
|
2516
|
+
node: ts.Expression,
|
|
2517
|
+
): { object: ts.Expression; shape: 'entries' | 'keys' | 'values' } | null {
|
|
2518
|
+
if (!ts.isCallExpression(node)) return null
|
|
2519
|
+
if (!ts.isPropertyAccessExpression(node.expression)) return null
|
|
2520
|
+
if (!ts.isIdentifier(node.expression.expression)) return null
|
|
2521
|
+
if (node.expression.expression.text !== 'Object') return null
|
|
2522
|
+
if (node.arguments.length !== 1) return null
|
|
2523
|
+
const name = node.expression.name.text
|
|
2524
|
+
if (name !== 'entries' && name !== 'keys' && name !== 'values') return null
|
|
2525
|
+
return { object: node.arguments[0], shape: name }
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2486
2528
|
type SortExtractionResult = {
|
|
2487
2529
|
result: IRLoopSort | null
|
|
2488
2530
|
unsupportedReason?: string
|
|
@@ -3246,6 +3288,10 @@ function transformMapCall(
|
|
|
3246
3288
|
// Capture nesting depth before we register this map's own params.
|
|
3247
3289
|
// ctx.loopParams is populated by the *outer* map; if non-empty we are inside one.
|
|
3248
3290
|
const isNested = ctx.loopParams.size > 0
|
|
3291
|
+
// This loop's own depth (0 = outermost) is however many enclosing
|
|
3292
|
+
// loops are already active, captured before `ctx.loopDepth` below is
|
|
3293
|
+
// bumped for THIS loop's own descendants.
|
|
3294
|
+
const depth = ctx.loopDepth
|
|
3249
3295
|
|
|
3250
3296
|
const propAccess = node.expression as ts.PropertyAccessExpression
|
|
3251
3297
|
const mapSource = propAccess.expression
|
|
@@ -3271,6 +3317,7 @@ function transformMapCall(
|
|
|
3271
3317
|
let templateMapPreamble: string | undefined
|
|
3272
3318
|
let typedMapPreamble: string | undefined
|
|
3273
3319
|
let iterationShape: 'entries' | 'keys' | undefined
|
|
3320
|
+
let objectIteration: 'entries' | 'keys' | 'values' | undefined
|
|
3274
3321
|
|
|
3275
3322
|
// Helper to set both array and templateArray
|
|
3276
3323
|
const setArray = (node: ts.Expression) => {
|
|
@@ -3284,8 +3331,11 @@ function transformMapCall(
|
|
|
3284
3331
|
// adapters emit the right loop variable bindings. `.values()` is a
|
|
3285
3332
|
// no-op (same as plain `.map()`) so it's stripped but not recorded.
|
|
3286
3333
|
// The inner expression (after stripping) feeds into the standard
|
|
3287
|
-
// filter/sort chain detection below.
|
|
3288
|
-
|
|
3334
|
+
// filter/sort chain detection below. Widened to `ts.Expression` (not
|
|
3335
|
+
// narrowed to `mapSource`'s own `LeftHandSideExpression` type) since
|
|
3336
|
+
// `isObjectIteratorCall`'s stripped argument can be any expression
|
|
3337
|
+
// (`x ?? {}`, not just a `LeftHandSideExpression`).
|
|
3338
|
+
let chainSource: ts.Expression = mapSource
|
|
3289
3339
|
const iteratorInfo = isIteratorShapeCall(mapSource)
|
|
3290
3340
|
if (iteratorInfo) {
|
|
3291
3341
|
chainSource = iteratorInfo.array
|
|
@@ -3295,6 +3345,18 @@ function transformMapCall(
|
|
|
3295
3345
|
iterationShape = 'keys'
|
|
3296
3346
|
}
|
|
3297
3347
|
// 'values' is a no-op — same as plain .map()
|
|
3348
|
+
} else {
|
|
3349
|
+
// Detect the STATIC `Object.entries(x)` / `.keys(x)` / `.values(x)`
|
|
3350
|
+
// form (#2168 object-entries-map) — see `isObjectIteratorCall`'s and
|
|
3351
|
+
// `IRLoop.objectIteration`'s docstrings for why this is a SEPARATE
|
|
3352
|
+
// shape from the array-instance-method case above, not a shared one.
|
|
3353
|
+
// Unlike that case, `'values'` DOES need recording here (it isn't a
|
|
3354
|
+
// no-op: `x` itself isn't iterable as a plain object).
|
|
3355
|
+
const objectIteratorInfo = isObjectIteratorCall(mapSource)
|
|
3356
|
+
if (objectIteratorInfo) {
|
|
3357
|
+
chainSource = objectIteratorInfo.object
|
|
3358
|
+
objectIteration = objectIteratorInfo.shape
|
|
3359
|
+
}
|
|
3298
3360
|
}
|
|
3299
3361
|
|
|
3300
3362
|
const filterInfo = isFilterCall(chainSource)
|
|
@@ -3445,8 +3507,12 @@ function transformMapCall(
|
|
|
3445
3507
|
// `.entries()` synthesises `[index, value]` — when the callback
|
|
3446
3508
|
// destructures exactly two array elements, extract the names into
|
|
3447
3509
|
// `index` and `param` so the loop renders with proper bindings and
|
|
3448
|
-
// the BF104 destructure-param refusal doesn't fire.
|
|
3449
|
-
|
|
3510
|
+
// the BF104 destructure-param refusal doesn't fire. `Object.entries(x)`
|
|
3511
|
+
// (`objectIteration === 'entries'`) synthesises the SAME `[key,
|
|
3512
|
+
// value]` 2-tuple shape — `index` just holds a string key instead
|
|
3513
|
+
// of a numeric position — so it reuses this exact extraction.
|
|
3514
|
+
const isEntriesShape = iterationShape === 'entries' || objectIteration === 'entries'
|
|
3515
|
+
if (isEntriesShape && ts.isArrayBindingPattern(firstParam.name)) {
|
|
3450
3516
|
const elements = firstParam.name.elements.filter(
|
|
3451
3517
|
el => !ts.isOmittedExpression(el),
|
|
3452
3518
|
)
|
|
@@ -3485,7 +3551,7 @@ function transformMapCall(
|
|
|
3485
3551
|
}
|
|
3486
3552
|
}
|
|
3487
3553
|
}
|
|
3488
|
-
if (callback.parameters.length > 1 && iterationShape !== 'entries') {
|
|
3554
|
+
if (callback.parameters.length > 1 && iterationShape !== 'entries' && objectIteration !== 'entries') {
|
|
3489
3555
|
const secondParam = callback.parameters[1]
|
|
3490
3556
|
index = secondParam.name.getText(ctx.sourceFile)
|
|
3491
3557
|
if (secondParam.type) {
|
|
@@ -3504,6 +3570,7 @@ function transformMapCall(
|
|
|
3504
3570
|
ctx.loopParams.add(param)
|
|
3505
3571
|
}
|
|
3506
3572
|
if (index) ctx.loopParams.add(index)
|
|
3573
|
+
ctx.loopDepth++
|
|
3507
3574
|
|
|
3508
3575
|
// Logical control flow (`cond && <X/>`, `a ?? themeLogo()`) as the map
|
|
3509
3576
|
// body. This is not a JSX literal, ternary, or block, so without this
|
|
@@ -3626,6 +3693,7 @@ function transformMapCall(
|
|
|
3626
3693
|
ctx.loopParams.delete(param)
|
|
3627
3694
|
}
|
|
3628
3695
|
if (index) ctx.loopParams.delete(index)
|
|
3696
|
+
ctx.loopDepth--
|
|
3629
3697
|
}
|
|
3630
3698
|
|
|
3631
3699
|
// If no JSX children were found (e.g., callback returns a function call),
|
|
@@ -3705,6 +3773,16 @@ function transformMapCall(
|
|
|
3705
3773
|
!isSignalOrMemoArray(array, ctx)
|
|
3706
3774
|
&& !isDirectPropArray
|
|
3707
3775
|
&& !hasCalls
|
|
3776
|
+
// `objectIteration` (#2168 object-entries-map): `array` here is the
|
|
3777
|
+
// STRIPPED object expression (`Object.entries(x)`'s `x`), which can
|
|
3778
|
+
// itself be a static module-scope const object literal and would
|
|
3779
|
+
// otherwise satisfy every check above — but a plain OBJECT has no
|
|
3780
|
+
// `.forEach()`/`.map()` (the static-array client codegen's own
|
|
3781
|
+
// methods), unlike an actual array literal. Force the dynamic
|
|
3782
|
+
// `mapArray()` path instead, which this shape's client-JS array-expr
|
|
3783
|
+
// reconstruction (`applyObjectIterationWrap`, `ir-to-client-js/utils.ts`)
|
|
3784
|
+
// already handles correctly.
|
|
3785
|
+
&& !objectIteration
|
|
3708
3786
|
|
|
3709
3787
|
// Collect nested components for both static and dynamic arrays.
|
|
3710
3788
|
// Static arrays: needed for initChild hydration.
|
|
@@ -3744,6 +3822,8 @@ function transformMapCall(
|
|
|
3744
3822
|
sortComparator,
|
|
3745
3823
|
chainOrder,
|
|
3746
3824
|
iterationShape,
|
|
3825
|
+
objectIteration,
|
|
3826
|
+
depth,
|
|
3747
3827
|
clientOnly: isClientOnly || undefined,
|
|
3748
3828
|
mapPreamble,
|
|
3749
3829
|
templateMapPreamble,
|
|
@@ -4157,9 +4237,12 @@ function getAttributeValue(attr: ts.JsxAttribute, ctx: TransformContext): AttrVa
|
|
|
4157
4237
|
return AttrValueOf.booleanAttr()
|
|
4158
4238
|
}
|
|
4159
4239
|
|
|
4160
|
-
// String literal: <div id="main"
|
|
4240
|
+
// String literal: <div id="main" />. JSX decodes character references
|
|
4241
|
+
// in quoted attribute values just like in text children, so the IR
|
|
4242
|
+
// carries the decoded string (`title="Fish & Chips"` IS the value
|
|
4243
|
+
// `Fish & Chips`); adapters re-escape on emit.
|
|
4161
4244
|
if (ts.isStringLiteral(attr.initializer)) {
|
|
4162
|
-
return AttrValueOf.literal(attr.initializer.text)
|
|
4245
|
+
return AttrValueOf.literal(decodeEntities(attr.initializer.text))
|
|
4163
4246
|
}
|
|
4164
4247
|
|
|
4165
4248
|
// Expression: <div class={className} />
|
package/src/types.ts
CHANGED
|
@@ -574,6 +574,74 @@ export interface IRLoop {
|
|
|
574
574
|
*/
|
|
575
575
|
iterationShape?: 'entries' | 'keys'
|
|
576
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Pre-`.map()` object iteration (#2168 object-entries-map). Distinct
|
|
579
|
+
* from {@link iterationShape}, which is scoped ENTIRELY to an array's
|
|
580
|
+
* own zero-arg `.entries()`/`.keys()`/`.values()` methods — those
|
|
581
|
+
* synthesize a real numeric index off the array's position, and every
|
|
582
|
+
* adapter's consumption of `iterationShape` assumes an actual
|
|
583
|
+
* array/slice underneath.
|
|
584
|
+
*
|
|
585
|
+
* `objectIteration` instead records the STATIC `Object.entries(x)` /
|
|
586
|
+
* `Object.keys(x)` / `Object.values(x)` call form, where `x` is a
|
|
587
|
+
* plain object/Record (not an array): the "index" bound for `'entries'`
|
|
588
|
+
* is a STRING KEY, not a numeric position, and the collection an
|
|
589
|
+
* adapter must iterate is its native map/dict/hash type, not an
|
|
590
|
+
* array/slice. `transformMapCall` strips the `Object.<method>(...)`
|
|
591
|
+
* wrapper the same way it strips `arr.entries()` — `array`/`arrayParsed`
|
|
592
|
+
* end up holding just `x` — and records the shape here so each
|
|
593
|
+
* adapter's loop renderer picks the right native construct:
|
|
594
|
+
*
|
|
595
|
+
* - `'entries'` → both `index` (bound to the KEY) and `param` (bound
|
|
596
|
+
* to the VALUE), synthesized from the 2-element destructure the
|
|
597
|
+
* same way `iterationShape: 'entries'` is (see `jsx-to-ir.ts`'s
|
|
598
|
+
* `transformMapCall`) — e.g. Jinja `for k, v in x.items()`.
|
|
599
|
+
* - `'keys'` → `param` bound to the key only — e.g. Jinja
|
|
600
|
+
* `for k in x.keys()`.
|
|
601
|
+
* - `'values'` → `param` bound to the value only — e.g. Jinja
|
|
602
|
+
* `for v in x.values()`. Unlike the array case, `'values'` is NOT
|
|
603
|
+
* a no-op here: `Object.values(x)` genuinely differs from
|
|
604
|
+
* iterating `x` itself (`x` isn't iterable at all as a plain
|
|
605
|
+
* object), so it must be recorded.
|
|
606
|
+
*
|
|
607
|
+
* Iteration ORDER is native-map-dependent: Python `dict`/PHP
|
|
608
|
+
* array-object/Ruby `Hash` preserve the source object's insertion
|
|
609
|
+
* order (matching JS `Object.entries()` semantics exactly), so Jinja,
|
|
610
|
+
* Twig, Blade, and ERB lower directly to their native map/dict/hash
|
|
611
|
+
* iteration. Go's `map[string]T`, Rust's `BTreeMap` (deliberate design,
|
|
612
|
+
* see `num.rs`), and Perl's hash (Xslate/Mojolicious) have NO
|
|
613
|
+
* order-preserving native map type, so those four instead lower to a
|
|
614
|
+
* DETERMINISTIC SORTED-BY-KEY iteration — Go's `{{range}}` (the
|
|
615
|
+
* stdlib's own `fmtsort`), minijinja's `BTreeMap` (already sorted),
|
|
616
|
+
* Kolon's `.kv()`/`.keys()`/`.values()` (verified empirically sorted),
|
|
617
|
+
* and Mojolicious's explicit `sort keys %$hash` (mirroring the
|
|
618
|
+
* existing `spread_attrs`/`_style_to_css` convention in
|
|
619
|
+
* `BarefootJS.pm`). This is a documented, permanent known limitation
|
|
620
|
+
* relative to JS's insertion-order guarantee (not a follow-up TODO) —
|
|
621
|
+
* true insertion order is physically unrecoverable from those
|
|
622
|
+
* languages' native map types once constructed, so sorted order is the
|
|
623
|
+
* best available deterministic approximation, not an interim refusal.
|
|
624
|
+
*
|
|
625
|
+
* Only the OUTERMOST, unchained `Object.<method>(x).map(cb)` shape is
|
|
626
|
+
* recognized — mirroring `iterationShape`'s own scope, chaining
|
|
627
|
+
* (`Object.entries(x).filter(pred).map(cb)`) is not (yet) recognized
|
|
628
|
+
* either, same as the array case.
|
|
629
|
+
*/
|
|
630
|
+
objectIteration?: 'entries' | 'keys' | 'values'
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Count of enclosing `.map()` loops (0 = outermost, 1 = nested one
|
|
634
|
+
* level deep, ...). Adapters use this to derive the loop body's
|
|
635
|
+
* `key`/`data-key` attribute suffix — `depth > 0 ? 'data-key-' +
|
|
636
|
+
* depth : 'data-key'` — matching `keyAttrName()` in
|
|
637
|
+
* `ir-to-client-js/utils.ts`, which the CSR path and the Hono SSR
|
|
638
|
+
* adapter each already derive independently (a recursion counter and
|
|
639
|
+
* a push/pop stack respectively). Before this field, the 8 template
|
|
640
|
+
* (non-JS) adapters had no depth awareness at all and always emitted
|
|
641
|
+
* plain `data-key` on nested-loop items (#2168 nested-loop-outer-binding).
|
|
642
|
+
*/
|
|
643
|
+
depth: number
|
|
644
|
+
|
|
577
645
|
/**
|
|
578
646
|
* When true, loop should be evaluated on client side only.
|
|
579
647
|
* SSR adapters should skip rendering and output placeholder markers.
|