@barefootjs/jsx 0.17.0 → 0.18.0
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/env-signal.d.ts +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/interface.d.ts +20 -4
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +19 -0
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +48 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +24 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1728 -969
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +41 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +22 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +55 -18
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +13 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/relocate.d.ts +28 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
- package/src/__tests__/augment-inherited-props.test.ts +96 -0
- package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
- package/src/__tests__/compiler-stress-1244.test.ts +13 -4
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
- package/src/__tests__/destructured-map-params.test.ts +11 -1
- package/src/__tests__/expression-parser.test.ts +74 -3
- package/src/__tests__/free-identifiers.test.ts +55 -0
- package/src/__tests__/ir-sort-comparator.test.ts +261 -0
- package/src/__tests__/loop-destructure.test.ts +313 -0
- package/src/__tests__/loop-hoisted-template.test.ts +235 -0
- package/src/__tests__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
- package/src/__tests__/ssr-defaults.test.ts +20 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
- package/src/__tests__/tagged-template-interleave.test.ts +268 -0
- package/src/__tests__/unsupported-expression.test.ts +194 -7
- package/src/adapters/env-signal.ts +57 -9
- package/src/adapters/interface.ts +20 -4
- package/src/adapters/parsed-expr-emitter.ts +19 -2
- package/src/analyzer-context.ts +20 -0
- package/src/analyzer.ts +74 -1
- package/src/augment-inherited-props.ts +139 -9
- package/src/compiler.ts +10 -1
- package/src/expression-parser.ts +421 -50
- package/src/index.ts +30 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -1
- package/src/ir-to-client-js/compute-inlinability.ts +6 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
- package/src/ir-to-client-js/emit-registration.ts +4 -2
- package/src/ir-to-client-js/html-template.ts +198 -1
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/types.ts +22 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/loop-destructure.ts +89 -36
- package/src/lowering-registry.ts +16 -0
- package/src/relocate.ts +201 -14
- package/src/ssr-defaults.ts +34 -32
- package/src/ssr-seed-plan.ts +146 -0
- package/src/types.ts +76 -0
|
@@ -382,3 +382,264 @@ describe('sort().map() / toSorted().map()', () => {
|
|
|
382
382
|
}
|
|
383
383
|
})
|
|
384
384
|
})
|
|
385
|
+
|
|
386
|
+
// #2090: `.sort(cb)` / `.toSorted(cb)` where `cb` is a bare identifier
|
|
387
|
+
// reference to a same-file arrow/function-declaration comparator, resolved
|
|
388
|
+
// through the analyzer's scope machinery (`findLocalConst` /
|
|
389
|
+
// `findLocalFunction` in jsx-to-ir.ts) one hop, then fed through the SAME
|
|
390
|
+
// `sortComparatorFromArrow` catalogue as an inline comparator — no new
|
|
391
|
+
// comparator shapes, no IR schema change.
|
|
392
|
+
describe('function-reference sort comparator (#2090)', () => {
|
|
393
|
+
test('const arrow reference, ascending', () => {
|
|
394
|
+
const source = `
|
|
395
|
+
'use client'
|
|
396
|
+
import { createSignal } from '@barefootjs/client'
|
|
397
|
+
|
|
398
|
+
export function ProductList() {
|
|
399
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
400
|
+
const byPrice = (a, b) => a.price - b.price
|
|
401
|
+
return (
|
|
402
|
+
<ul>
|
|
403
|
+
{products().sort(byPrice).map(p => (
|
|
404
|
+
<li key={p.name}>{p.name}</li>
|
|
405
|
+
))}
|
|
406
|
+
</ul>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
`
|
|
410
|
+
|
|
411
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
412
|
+
const ir = jsxToIR(ctx)
|
|
413
|
+
|
|
414
|
+
expect(ir).not.toBeNull()
|
|
415
|
+
if (ir!.type === 'element') {
|
|
416
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
417
|
+
expect(loop).toBeDefined()
|
|
418
|
+
if (loop?.type === 'loop') {
|
|
419
|
+
expect(loop.sortComparator).toBeDefined()
|
|
420
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
421
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
|
|
422
|
+
])
|
|
423
|
+
expect(loop.sortComparator!.paramA).toBe('a')
|
|
424
|
+
expect(loop.sortComparator!.paramB).toBe('b')
|
|
425
|
+
expect(loop.sortComparator!.raw).toBe('a.price - b.price')
|
|
426
|
+
expect(loop.array).toBe('products()')
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('const arrow reference, descending multi-key (||-chain)', () => {
|
|
432
|
+
const source = `
|
|
433
|
+
'use client'
|
|
434
|
+
import { createSignal } from '@barefootjs/client'
|
|
435
|
+
|
|
436
|
+
export function ProductList() {
|
|
437
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
438
|
+
const byPriceThenName = (a, b) => b.price - a.price || a.name.localeCompare(b.name)
|
|
439
|
+
return (
|
|
440
|
+
<ul>
|
|
441
|
+
{products().sort(byPriceThenName).map(p => (
|
|
442
|
+
<li key={p.name}>{p.name}</li>
|
|
443
|
+
))}
|
|
444
|
+
</ul>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
`
|
|
448
|
+
|
|
449
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
450
|
+
const ir = jsxToIR(ctx)
|
|
451
|
+
|
|
452
|
+
expect(ir).not.toBeNull()
|
|
453
|
+
if (ir!.type === 'element') {
|
|
454
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
455
|
+
if (loop?.type === 'loop') {
|
|
456
|
+
expect(loop.sortComparator).toBeDefined()
|
|
457
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
458
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
|
|
459
|
+
{ key: { kind: 'field', field: 'name' }, type: 'string', direction: 'asc' },
|
|
460
|
+
])
|
|
461
|
+
expect(loop.sortComparator!.raw).toBe('b.price - a.price || a.name.localeCompare(b.name)')
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('function-declaration reference with return-block body', () => {
|
|
467
|
+
const source = `
|
|
468
|
+
'use client'
|
|
469
|
+
import { createSignal } from '@barefootjs/client'
|
|
470
|
+
|
|
471
|
+
function byPrice(a, b) {
|
|
472
|
+
return a.price - b.price
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function ProductList() {
|
|
476
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
477
|
+
return (
|
|
478
|
+
<ul>
|
|
479
|
+
{products().sort(byPrice).map(p => (
|
|
480
|
+
<li key={p.name}>{p.name}</li>
|
|
481
|
+
))}
|
|
482
|
+
</ul>
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
`
|
|
486
|
+
|
|
487
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
488
|
+
const ir = jsxToIR(ctx)
|
|
489
|
+
|
|
490
|
+
expect(ir).not.toBeNull()
|
|
491
|
+
if (ir!.type === 'element') {
|
|
492
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
493
|
+
if (loop?.type === 'loop') {
|
|
494
|
+
expect(loop.sortComparator).toBeDefined()
|
|
495
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
496
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
|
|
497
|
+
])
|
|
498
|
+
expect(loop.sortComparator!.paramA).toBe('a')
|
|
499
|
+
expect(loop.sortComparator!.paramB).toBe('b')
|
|
500
|
+
expect(loop.sortComparator!.raw).toBe('a.price - b.price')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test('.toSorted(ref) resolves the same way as .sort(ref)', () => {
|
|
506
|
+
const source = `
|
|
507
|
+
'use client'
|
|
508
|
+
import { createSignal } from '@barefootjs/client'
|
|
509
|
+
|
|
510
|
+
export function ProductList() {
|
|
511
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
512
|
+
const byPrice = (a, b) => a.price - b.price
|
|
513
|
+
return (
|
|
514
|
+
<ul>
|
|
515
|
+
{products().toSorted(byPrice).map(p => (
|
|
516
|
+
<li key={p.name}>{p.name}</li>
|
|
517
|
+
))}
|
|
518
|
+
</ul>
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
`
|
|
522
|
+
|
|
523
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
524
|
+
const ir = jsxToIR(ctx)
|
|
525
|
+
|
|
526
|
+
expect(ir).not.toBeNull()
|
|
527
|
+
if (ir!.type === 'element') {
|
|
528
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
529
|
+
if (loop?.type === 'loop') {
|
|
530
|
+
expect(loop.sortComparator).toBeDefined()
|
|
531
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
532
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
|
|
533
|
+
])
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
test('component-scope const shadows a module-scope const of the same name', () => {
|
|
539
|
+
const source = `
|
|
540
|
+
'use client'
|
|
541
|
+
import { createSignal } from '@barefootjs/client'
|
|
542
|
+
|
|
543
|
+
const byPrice = (a, b) => a.price - b.price
|
|
544
|
+
|
|
545
|
+
export function ProductList() {
|
|
546
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
547
|
+
const byPrice = (a, b) => b.price - a.price
|
|
548
|
+
return (
|
|
549
|
+
<ul>
|
|
550
|
+
{products().sort(byPrice).map(p => (
|
|
551
|
+
<li key={p.name}>{p.name}</li>
|
|
552
|
+
))}
|
|
553
|
+
</ul>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
`
|
|
557
|
+
|
|
558
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
559
|
+
const ir = jsxToIR(ctx)
|
|
560
|
+
|
|
561
|
+
expect(ir).not.toBeNull()
|
|
562
|
+
if (ir!.type === 'element') {
|
|
563
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
564
|
+
if (loop?.type === 'loop') {
|
|
565
|
+
expect(loop.sortComparator).toBeDefined()
|
|
566
|
+
// The component-scope binding (descending) wins over the
|
|
567
|
+
// module-scope one (ascending) — distinguishable via `direction`.
|
|
568
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
569
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
|
|
570
|
+
])
|
|
571
|
+
expect(loop.sortComparator!.raw).toBe('b.price - a.price')
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test('filter().sort(ref).map() resolves the reference in a chain', () => {
|
|
577
|
+
const source = `
|
|
578
|
+
'use client'
|
|
579
|
+
import { createSignal } from '@barefootjs/client'
|
|
580
|
+
|
|
581
|
+
export function TodoList() {
|
|
582
|
+
const [todos, setTodos] = createSignal<any[]>([])
|
|
583
|
+
const byPriority = (a, b) => a.priority - b.priority
|
|
584
|
+
return (
|
|
585
|
+
<ul>
|
|
586
|
+
{todos().filter(t => !t.done).sort(byPriority).map(t => (
|
|
587
|
+
<li key={t.text}>{t.text}</li>
|
|
588
|
+
))}
|
|
589
|
+
</ul>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
`
|
|
593
|
+
|
|
594
|
+
const ctx = analyzeComponent(source, 'TodoList.tsx')
|
|
595
|
+
const ir = jsxToIR(ctx)
|
|
596
|
+
|
|
597
|
+
expect(ir).not.toBeNull()
|
|
598
|
+
if (ir!.type === 'element') {
|
|
599
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
600
|
+
if (loop?.type === 'loop') {
|
|
601
|
+
expect(loop.filterPredicate).toBeDefined()
|
|
602
|
+
expect(loop.sortComparator).toBeDefined()
|
|
603
|
+
expect(loop.chainOrder).toBe('filter-sort')
|
|
604
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
605
|
+
{ key: { kind: 'field', field: 'priority' }, type: 'numeric', direction: 'asc' },
|
|
606
|
+
])
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('sort(ref).filter().map() resolves the reference in a chain', () => {
|
|
612
|
+
const source = `
|
|
613
|
+
'use client'
|
|
614
|
+
import { createSignal } from '@barefootjs/client'
|
|
615
|
+
|
|
616
|
+
export function TodoList() {
|
|
617
|
+
const [todos, setTodos] = createSignal<any[]>([])
|
|
618
|
+
const byPriority = (a, b) => a.priority - b.priority
|
|
619
|
+
return (
|
|
620
|
+
<ul>
|
|
621
|
+
{todos().sort(byPriority).filter(t => !t.done).map(t => (
|
|
622
|
+
<li key={t.text}>{t.text}</li>
|
|
623
|
+
))}
|
|
624
|
+
</ul>
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
`
|
|
628
|
+
|
|
629
|
+
const ctx = analyzeComponent(source, 'TodoList.tsx')
|
|
630
|
+
const ir = jsxToIR(ctx)
|
|
631
|
+
|
|
632
|
+
expect(ir).not.toBeNull()
|
|
633
|
+
if (ir!.type === 'element') {
|
|
634
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
635
|
+
if (loop?.type === 'loop') {
|
|
636
|
+
expect(loop.filterPredicate).toBeDefined()
|
|
637
|
+
expect(loop.sortComparator).toBeDefined()
|
|
638
|
+
expect(loop.chainOrder).toBe('sort-filter')
|
|
639
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
640
|
+
{ key: { kind: 'field', field: 'priority' }, type: 'numeric', direction: 'asc' },
|
|
641
|
+
])
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
})
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for #2087 Phase A: `isLowerableLoopDestructure` (née
|
|
3
|
+
* `isLowerableObjectRestDestructure`) — the gate template adapters use to
|
|
4
|
+
* decide whether a `.map()` callback's destructure param can be lowered
|
|
5
|
+
* natively, versus falling back to the BF104 diagnostic.
|
|
6
|
+
*
|
|
7
|
+
* Compiles a small component to IR (`analyzeComponent` + `jsxToIR`, the same
|
|
8
|
+
* helper pattern as `ir-sort-comparator.test.ts`), locates the `IRLoop`
|
|
9
|
+
* node, and asserts the gate's verdict plus (where relevant) the structured
|
|
10
|
+
* `segments` path each binding carries.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect } from 'bun:test'
|
|
14
|
+
import { analyzeComponent } from '../analyzer'
|
|
15
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
16
|
+
import { isLowerableLoopDestructure, isLowerableObjectRestDestructure } from '../loop-destructure'
|
|
17
|
+
import { ErrorCodes } from '../errors'
|
|
18
|
+
import type { IRLoop, IRNode } from '../types'
|
|
19
|
+
|
|
20
|
+
function findFirstLoop(node: IRNode): IRLoop {
|
|
21
|
+
if (node.type === 'loop') return node
|
|
22
|
+
if (node.type === 'element' || node.type === 'fragment') {
|
|
23
|
+
for (const child of node.children) {
|
|
24
|
+
if (child.type === 'loop') return child
|
|
25
|
+
if (child.type === 'element' || child.type === 'fragment') {
|
|
26
|
+
try {
|
|
27
|
+
return findFirstLoop(child)
|
|
28
|
+
} catch {
|
|
29
|
+
// keep scanning siblings
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error('no loop node found in IR')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compileLoop(
|
|
38
|
+
source: string,
|
|
39
|
+
targetComponentName?: string,
|
|
40
|
+
filename = 'Test.tsx',
|
|
41
|
+
): { loop: IRLoop; errors: ReturnType<typeof analyzeComponent>['errors'] } {
|
|
42
|
+
const ctx = analyzeComponent(source, filename, targetComponentName)
|
|
43
|
+
const ir = jsxToIR(ctx)
|
|
44
|
+
expect(ir).not.toBeNull()
|
|
45
|
+
return { loop: findFirstLoop(ir!), errors: ctx.errors }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('isLowerableLoopDestructure (#2087)', () => {
|
|
49
|
+
test('simple .field bindings + object-rest read via member access → true (existing behavior)', () => {
|
|
50
|
+
const { loop } = compileLoop(`
|
|
51
|
+
'use client'
|
|
52
|
+
import { createSignal } from '@barefootjs/client'
|
|
53
|
+
|
|
54
|
+
type Task = { id: string; title: string; flag: string }
|
|
55
|
+
export function RestObject() {
|
|
56
|
+
const [tasks, setTasks] = createSignal<Task[]>([])
|
|
57
|
+
return (
|
|
58
|
+
<ul onClick={() => setTasks(t => t)}>
|
|
59
|
+
{tasks().map(({ id, title, ...rest }) => (
|
|
60
|
+
<li key={id}>{title}:{rest.flag}</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
`)
|
|
66
|
+
expect(isLowerableLoopDestructure(loop)).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('array-index tuple destructure ([k, v]) → true (NEW)', () => {
|
|
70
|
+
const { loop } = compileLoop(`
|
|
71
|
+
'use client'
|
|
72
|
+
import { createSignal } from '@barefootjs/client'
|
|
73
|
+
|
|
74
|
+
type Row = readonly [string, string]
|
|
75
|
+
export function IndexPairs() {
|
|
76
|
+
const [rows, setRows] = createSignal<Row[]>([])
|
|
77
|
+
return (
|
|
78
|
+
<ul onClick={() => setRows(r => r)}>
|
|
79
|
+
{rows().map(([k, v]) => (
|
|
80
|
+
<li key={k}>{k}:{v}</li>
|
|
81
|
+
))}
|
|
82
|
+
</ul>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
`)
|
|
86
|
+
expect(isLowerableLoopDestructure(loop)).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('nested { id, cells: [head] } → true (NEW)', () => {
|
|
90
|
+
const { loop } = compileLoop(`
|
|
91
|
+
'use client'
|
|
92
|
+
import { createSignal } from '@barefootjs/client'
|
|
93
|
+
|
|
94
|
+
type Row = { id: string; cells: readonly string[] }
|
|
95
|
+
export function NestedHead() {
|
|
96
|
+
const [rows, setRows] = createSignal<Row[]>([])
|
|
97
|
+
return (
|
|
98
|
+
<ul onClick={() => setRows(r => r)}>
|
|
99
|
+
{rows().map(({ id, cells: [head] }) => (
|
|
100
|
+
<li key={id}>{head}</li>
|
|
101
|
+
))}
|
|
102
|
+
</ul>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
`)
|
|
106
|
+
expect(isLowerableLoopDestructure(loop)).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('array-rest [first, ...tail] with tail.length read → true (NEW)', () => {
|
|
110
|
+
const { loop } = compileLoop(`
|
|
111
|
+
'use client'
|
|
112
|
+
import { createSignal } from '@barefootjs/client'
|
|
113
|
+
|
|
114
|
+
export function RestTuple() {
|
|
115
|
+
const [rows, setRows] = createSignal<string[][]>([])
|
|
116
|
+
return (
|
|
117
|
+
<ul onClick={() => setRows(r => r)}>
|
|
118
|
+
{rows().map(([first, ...tail]) => (
|
|
119
|
+
<li key={first}>{first} (+{tail.length})</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
`)
|
|
125
|
+
expect(isLowerableLoopDestructure(loop)).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('object-rest {...rest} spread onto the loop-item root <li> → true (NEW)', () => {
|
|
129
|
+
const { loop } = compileLoop(`
|
|
130
|
+
'use client'
|
|
131
|
+
import { createSignal } from '@barefootjs/client'
|
|
132
|
+
|
|
133
|
+
type Task = { id: string; title: string; flag: string }
|
|
134
|
+
export function RestSpread() {
|
|
135
|
+
const [tasks, setTasks] = createSignal<Task[]>([])
|
|
136
|
+
return (
|
|
137
|
+
<ul onClick={() => setTasks(t => t)}>
|
|
138
|
+
{tasks().map(({ id, title, ...rest }) => (
|
|
139
|
+
<li key={id} {...rest}>{title}</li>
|
|
140
|
+
))}
|
|
141
|
+
</ul>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
`)
|
|
145
|
+
expect(isLowerableLoopDestructure(loop)).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('object-rest spread on a COMPONENT (<Child {...rest} />) → false', () => {
|
|
149
|
+
const { loop } = compileLoop(`
|
|
150
|
+
'use client'
|
|
151
|
+
import { createSignal } from '@barefootjs/client'
|
|
152
|
+
|
|
153
|
+
type Task = { id: string; title: string; flag: string }
|
|
154
|
+
function Child(props: { title: string; flag: string }) {
|
|
155
|
+
return <span>{props.title}</span>
|
|
156
|
+
}
|
|
157
|
+
export function RestOntoComponent() {
|
|
158
|
+
const [tasks, setTasks] = createSignal<Task[]>([])
|
|
159
|
+
return (
|
|
160
|
+
<ul onClick={() => setTasks(t => t)}>
|
|
161
|
+
{tasks().map(({ id, ...rest }) => (
|
|
162
|
+
<li key={id}><Child {...rest} /></li>
|
|
163
|
+
))}
|
|
164
|
+
</ul>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
`, 'RestOntoComponent')
|
|
168
|
+
expect(isLowerableLoopDestructure(loop)).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('object-rest bare value use in a text expression ({String(rest)}) → false', () => {
|
|
172
|
+
const { loop } = compileLoop(`
|
|
173
|
+
'use client'
|
|
174
|
+
import { createSignal } from '@barefootjs/client'
|
|
175
|
+
|
|
176
|
+
type Task = { id: string; title: string; flag: string }
|
|
177
|
+
export function RestBareText() {
|
|
178
|
+
const [tasks, setTasks] = createSignal<Task[]>([])
|
|
179
|
+
return (
|
|
180
|
+
<ul onClick={() => setTasks(t => t)}>
|
|
181
|
+
{tasks().map(({ id, ...rest }) => (
|
|
182
|
+
<li key={id}>{String(rest)}</li>
|
|
183
|
+
))}
|
|
184
|
+
</ul>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
`)
|
|
188
|
+
expect(isLowerableLoopDestructure(loop)).toBe(false)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('object-rest bare value use in an event handler (onClick={() => fn(rest)}) → false', () => {
|
|
192
|
+
const { loop } = compileLoop(`
|
|
193
|
+
'use client'
|
|
194
|
+
import { createSignal } from '@barefootjs/client'
|
|
195
|
+
|
|
196
|
+
type Task = { id: string; title: string; flag: string }
|
|
197
|
+
export function RestBareHandler() {
|
|
198
|
+
const [tasks, setTasks] = createSignal<Task[]>([])
|
|
199
|
+
const fn = (t: unknown) => {}
|
|
200
|
+
return (
|
|
201
|
+
<ul>
|
|
202
|
+
{tasks().map(({ id, ...rest }) => (
|
|
203
|
+
<li key={id} onClick={() => fn(rest)}>{id}</li>
|
|
204
|
+
))}
|
|
205
|
+
</ul>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
`)
|
|
209
|
+
expect(isLowerableLoopDestructure(loop)).toBe(false)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('.filter(...).map(({a}) => ...) chain → false', () => {
|
|
213
|
+
const { loop } = compileLoop(`
|
|
214
|
+
'use client'
|
|
215
|
+
import { createSignal } from '@barefootjs/client'
|
|
216
|
+
|
|
217
|
+
type Task = { id: string; done: boolean }
|
|
218
|
+
export function FilterChain() {
|
|
219
|
+
const [tasks, setTasks] = createSignal<Task[]>([])
|
|
220
|
+
return (
|
|
221
|
+
<ul onClick={() => setTasks(t => t)}>
|
|
222
|
+
{tasks().filter(t => !t.done).map(({ id }) => (
|
|
223
|
+
<li key={id}>{id}</li>
|
|
224
|
+
))}
|
|
225
|
+
</ul>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
`)
|
|
229
|
+
expect(isLowerableLoopDestructure(loop)).toBe(false)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('binding named __bf_item → false', () => {
|
|
233
|
+
const { loop } = compileLoop(`
|
|
234
|
+
'use client'
|
|
235
|
+
import { createSignal } from '@barefootjs/client'
|
|
236
|
+
|
|
237
|
+
export function ReservedName() {
|
|
238
|
+
const [rows, setRows] = createSignal<{ __bf_item: string }[]>([])
|
|
239
|
+
return (
|
|
240
|
+
<ul onClick={() => setRows(r => r)}>
|
|
241
|
+
{rows().map(({ __bf_item }) => (
|
|
242
|
+
<li key={__bf_item}>{__bf_item}</li>
|
|
243
|
+
))}
|
|
244
|
+
</ul>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
`)
|
|
248
|
+
expect(isLowerableLoopDestructure(loop)).toBe(false)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('computed property key still raises BF025 and produces no paramBindings (unchanged)', () => {
|
|
252
|
+
const { loop, errors } = compileLoop(`
|
|
253
|
+
'use client'
|
|
254
|
+
import { createSignal } from '@barefootjs/client'
|
|
255
|
+
|
|
256
|
+
const KEY = 'a' as const
|
|
257
|
+
export function Computed() {
|
|
258
|
+
const [items, setItems] = createSignal<Record<string, number>[]>([])
|
|
259
|
+
return (
|
|
260
|
+
<ul onClick={() => setItems(i => i)}>
|
|
261
|
+
{items().map(({ [KEY]: v }) => <li key={v}>{v}</li>)}
|
|
262
|
+
</ul>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
`)
|
|
266
|
+
const bf025 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_DESTRUCTURE_REST)
|
|
267
|
+
expect(bf025.length).toBe(1)
|
|
268
|
+
expect(bf025[0].severity).toBe('error')
|
|
269
|
+
expect(loop.paramBindings).toBeUndefined()
|
|
270
|
+
expect(isLowerableLoopDestructure(loop)).toBe(false)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('deprecated alias isLowerableObjectRestDestructure is the same function', () => {
|
|
274
|
+
expect(isLowerableObjectRestDestructure).toBe(isLowerableLoopDestructure)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('segments: nested { id, cells: [head, ...rest] } pins field/index/non-ident classification', () => {
|
|
278
|
+
const { loop } = compileLoop(`
|
|
279
|
+
'use client'
|
|
280
|
+
import { createSignal } from '@barefootjs/client'
|
|
281
|
+
|
|
282
|
+
type Row = { id: string; cells: readonly string[]; 'data-priority': string }
|
|
283
|
+
export function NestedSegments() {
|
|
284
|
+
const [rows, setRows] = createSignal<Row[]>([])
|
|
285
|
+
return (
|
|
286
|
+
<ul onClick={() => setRows(r => r)}>
|
|
287
|
+
{rows().map(({ id, cells: [head, ...rest], 'data-priority': prio }) => (
|
|
288
|
+
<li key={id}>{head}:{String(rest.length)}:{prio}</li>
|
|
289
|
+
))}
|
|
290
|
+
</ul>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
`)
|
|
294
|
+
const bindings = loop.paramBindings
|
|
295
|
+
expect(bindings).toBeDefined()
|
|
296
|
+
|
|
297
|
+
const idBinding = bindings!.find(b => b.name === 'id')
|
|
298
|
+
expect(idBinding?.segments).toEqual([{ kind: 'field', key: 'id', isIdent: true }])
|
|
299
|
+
|
|
300
|
+
const headBinding = bindings!.find(b => b.name === 'head')
|
|
301
|
+
expect(headBinding?.segments).toEqual([
|
|
302
|
+
{ kind: 'field', key: 'cells', isIdent: true },
|
|
303
|
+
{ kind: 'index', index: 0 },
|
|
304
|
+
])
|
|
305
|
+
|
|
306
|
+
const restBinding = bindings!.find(b => b.name === 'rest')
|
|
307
|
+
expect(restBinding?.rest?.kind).toBe('array')
|
|
308
|
+
expect(restBinding?.segments).toEqual([{ kind: 'field', key: 'cells', isIdent: true }])
|
|
309
|
+
|
|
310
|
+
const prioBinding = bindings!.find(b => b.name === 'prio')
|
|
311
|
+
expect(prioBinding?.segments).toEqual([{ kind: 'field', key: 'data-priority', isIdent: false }])
|
|
312
|
+
})
|
|
313
|
+
})
|