@barefootjs/jsx 0.24.1 → 0.26.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/dangerous-inner-html.d.ts +52 -24
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +473 -165
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +24 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +13 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +31 -10
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +29 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
- package/src/__tests__/nested-loop-conditional.test.ts +133 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
- package/src/__tests__/reactive-factory-cross-file.test.ts +833 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
- package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +607 -142
- package/src/errors.ts +4 -0
- package/src/ir-to-client-js/collect-elements.ts +28 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
- package/src/ir-to-client-js/reactivity.ts +61 -7
- package/src/ir-to-client-js/types.ts +13 -0
- package/src/rich-type-refusal.ts +3 -2
- package/src/to-locale-date-lowering.ts +50 -13
- package/src/types.ts +30 -5
|
@@ -45,13 +45,18 @@ describe('resolveDangerousInnerHtml (#2207)', () => {
|
|
|
45
45
|
expect(resolveDangerousInnerHtml(el)).toEqual({ kind: 'static', html: '<i>x</i>' })
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
test('signal/prop-derived value resolves as dynamic', () => {
|
|
48
|
+
test('signal/prop-derived value resolves as dynamic and carries the inner expression (#2319)', () => {
|
|
49
49
|
const el = rootElement(`
|
|
50
50
|
function Test({ html }: { html: string }) { return <div dangerouslySetInnerHTML={{ __html: html }} /> }
|
|
51
51
|
export { Test }
|
|
52
52
|
`)
|
|
53
53
|
const resolution = resolveDangerousInnerHtml(el)
|
|
54
54
|
expect(resolution?.kind).toBe('dynamic')
|
|
55
|
+
// The adapter lowers `valueExpr`/`valueParsed` (the inner `__html` value),
|
|
56
|
+
// not the whole `{ __html: … }` object, through its raw-output sink.
|
|
57
|
+
if (resolution?.kind !== 'dynamic') throw new Error('expected dynamic')
|
|
58
|
+
expect(resolution.valueExpr).toBe('html')
|
|
59
|
+
expect(resolution.valueParsed).toEqual({ kind: 'identifier', name: 'html' })
|
|
55
60
|
})
|
|
56
61
|
|
|
57
62
|
test('a no-substitution template literal resolves as static (identical to a string literal)', () => {
|
|
@@ -87,12 +92,14 @@ describe('resolveDangerousInnerHtml (#2207)', () => {
|
|
|
87
92
|
expect(resolveDangerousInnerHtml(el)?.kind).toBe('dynamic')
|
|
88
93
|
})
|
|
89
94
|
|
|
90
|
-
test('extra property alongside __html resolves as
|
|
95
|
+
test('extra property alongside __html resolves as unlowerable (not a { __html } object literal)', () => {
|
|
91
96
|
const el = rootElement(`
|
|
92
97
|
function Test() { return <div dangerouslySetInnerHTML={{ __html: '<b>x</b>', extra: 1 }} /> }
|
|
93
98
|
export { Test }
|
|
94
99
|
`)
|
|
95
|
-
|
|
100
|
+
// Not the canonical single-property shape — no faithful lowering, so the
|
|
101
|
+
// adapter refuses with BF101 (#2319 lowers a lone `{ __html: expr }`).
|
|
102
|
+
expect(resolveDangerousInnerHtml(el)?.kind).toBe('unlowerable')
|
|
96
103
|
})
|
|
97
104
|
|
|
98
105
|
test('isDangerousInnerHtmlAttr only matches the exact prop name', () => {
|
|
@@ -187,11 +194,24 @@ describe('dangerousInnerHtmlMetacharViolation (#2207)', () => {
|
|
|
187
194
|
})
|
|
188
195
|
|
|
189
196
|
describe('dangerousInnerHtmlDiagnostic (#2207)', () => {
|
|
190
|
-
test('
|
|
191
|
-
const diag = dangerousInnerHtmlDiagnostic('
|
|
197
|
+
test('a wrong-shape refusal (no reason) states the single-__html-property contract', () => {
|
|
198
|
+
const diag = dangerousInnerHtmlDiagnostic('true', { file: 'Test.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } })
|
|
192
199
|
expect(diag.code).toBe('BF101')
|
|
193
200
|
expect(diag.severity).toBe('error')
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
// The message names the canonical shape (a single `__html` property) so a
|
|
202
|
+
// near-miss like `{ __html: x, extra: 1 }` isn't told it "expects an
|
|
203
|
+
// { __html: … }" it already appears to have (#2319 review).
|
|
204
|
+
expect(diag.message).toContain('requires an object literal with a single `__html` property')
|
|
205
|
+
// A dynamic value is now lowered, not refused, so the escape-hatch text
|
|
206
|
+
// points at the object-literal contract and /* @client */ (#2319).
|
|
207
|
+
expect(diag.suggestion?.message).toContain('@client')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('a valid-shape-but-unlowerable refusal (with reason) leads with the reason, not the shape', () => {
|
|
211
|
+
const diag = dangerousInnerHtmlDiagnostic('{ __html: `<b>${x}</b>` }', { file: 'Test.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }, 'no single-argument raw form on the Go adapter')
|
|
212
|
+
expect(diag.code).toBe('BF101')
|
|
213
|
+
expect(diag.message).toContain('cannot be lowered on this adapter — no single-argument raw form on the Go adapter')
|
|
214
|
+
// It must NOT claim the shape is wrong when it isn't.
|
|
215
|
+
expect(diag.message).not.toContain('requires an object literal with a single')
|
|
196
216
|
})
|
|
197
217
|
})
|
|
@@ -381,3 +381,136 @@ describe('nested loops/conditionals inside mapArray (#830, #839)', () => {
|
|
|
381
381
|
expect(outsideInitChildCount).toBe(0)
|
|
382
382
|
})
|
|
383
383
|
})
|
|
384
|
+
|
|
385
|
+
describe('per-item conditional wrapping a dynamic attr/event element binds exactly once (#2347)', () => {
|
|
386
|
+
// A per-item conditional (`cond ? null : <el/>`) whose element itself has a
|
|
387
|
+
// dynamic attribute and/or event handler used to get bound twice: once
|
|
388
|
+
// directly against the loop item's own initial template clone (querying
|
|
389
|
+
// `qsa(__el, ...)` right in the mapArray callback), and again inside the
|
|
390
|
+
// conditional's own `insert()` bindEvents against whatever node it mounts.
|
|
391
|
+
// At runtime this doubled the click handler and left the attribute effect
|
|
392
|
+
// pointed at a detached node once insert() swapped branches.
|
|
393
|
+
|
|
394
|
+
test('Repro 1: reactive className on a conditionally-rendered element binds once, inside bindEvents', () => {
|
|
395
|
+
const source = `
|
|
396
|
+
'use client'
|
|
397
|
+
import { createSignal } from '@barefootjs/client'
|
|
398
|
+
|
|
399
|
+
interface Item { handle: string; active: 0 | 1 }
|
|
400
|
+
|
|
401
|
+
export function List(props: { viewerHandle: string }) {
|
|
402
|
+
const [items, setItems] = createSignal<Item[]>([])
|
|
403
|
+
return (
|
|
404
|
+
<div>
|
|
405
|
+
{items().map(u => (
|
|
406
|
+
<div key={u.handle}>
|
|
407
|
+
{u.handle === props.viewerHandle ? null : (
|
|
408
|
+
<button className={u.active ? 'btn on' : 'btn'} onClick={() => {}}>
|
|
409
|
+
{u.active ? 'On' : 'Off'}
|
|
410
|
+
</button>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
))}
|
|
414
|
+
</div>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
`
|
|
418
|
+
const result = compileJSX(source, 'List.tsx', { adapter })
|
|
419
|
+
expect(result.errors).toHaveLength(0)
|
|
420
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
421
|
+
|
|
422
|
+
// Exactly one class effect, exactly one click listener.
|
|
423
|
+
expect((content.match(/setAttribute\('class'/g) ?? []).length).toBe(1)
|
|
424
|
+
expect((content.match(/addEventListener\('click'/g) ?? []).length).toBe(1)
|
|
425
|
+
|
|
426
|
+
// The class effect must live inside the conditional's own bindEvents
|
|
427
|
+
// (querying `__branchScope`, the node insert() actually mounts) —
|
|
428
|
+
// not directly against `__el` in the outer mapArray scope, which would
|
|
429
|
+
// go stale the moment insert() swaps in a fresh clone.
|
|
430
|
+
const bindEventsIndex = content.indexOf('bindEvents:')
|
|
431
|
+
const classEffectIndex = content.indexOf("setAttribute('class'")
|
|
432
|
+
expect(bindEventsIndex).toBeGreaterThan(-1)
|
|
433
|
+
expect(classEffectIndex).toBeGreaterThan(bindEventsIndex)
|
|
434
|
+
expect(content).toContain("qsa(__branchScope, '[bf=\"s2\"]')")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
test('Repro 2: a doubly-nested per-item conditional binds click exactly once, in the innermost arm', () => {
|
|
438
|
+
const source = `
|
|
439
|
+
'use client'
|
|
440
|
+
import { createSignal } from '@barefootjs/client'
|
|
441
|
+
|
|
442
|
+
interface Item { handle: string; active: 0 | 1 }
|
|
443
|
+
|
|
444
|
+
export function List(props: { viewerHandle: string; signedIn: boolean }) {
|
|
445
|
+
const [items, setItems] = createSignal<Item[]>([])
|
|
446
|
+
const onClick = (u: Item) => {}
|
|
447
|
+
return (
|
|
448
|
+
<div>
|
|
449
|
+
{items().map(u => (
|
|
450
|
+
<div key={u.handle}>
|
|
451
|
+
{u.handle === props.viewerHandle ? null : props.signedIn ? (
|
|
452
|
+
<button className={u.active ? 'btn on' : 'btn'} onClick={() => onClick(u)}>
|
|
453
|
+
{u.active ? 'On' : 'Off'}
|
|
454
|
+
</button>
|
|
455
|
+
) : (
|
|
456
|
+
<a href="/login" rel="nofollow noreferrer">Off</a>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
))}
|
|
460
|
+
</div>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
`
|
|
464
|
+
const result = compileJSX(source, 'List.tsx', { adapter })
|
|
465
|
+
expect(result.errors).toHaveLength(0)
|
|
466
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
467
|
+
|
|
468
|
+
// A single click bound in the browser used to fire the handler twice —
|
|
469
|
+
// once from the outer arm's own qsa()+addEventListener, once again from
|
|
470
|
+
// the nested insert()'s bindEvents — silently double-toggling state.
|
|
471
|
+
expect((content.match(/addEventListener\('click'/g) ?? []).length).toBe(1)
|
|
472
|
+
expect((content.match(/setAttribute\('class'/g) ?? []).length).toBe(1)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('a JSX-callback prop call as the WHOLE branch content does not get a nested text effect', () => {
|
|
476
|
+
// Regression pin for a fix-of-the-fix: the initial #2347 patch made
|
|
477
|
+
// `summarizeLoopChildBranch` collect reactive texts for any branch,
|
|
478
|
+
// which newly reached a per-item conditional whose branch is a single
|
|
479
|
+
// bare `expression` (no wrapping element) — e.g. a hoisted JSX-callback
|
|
480
|
+
// prop call (`renderNode={(n) => <Pill/>}`-shaped, #1211/#1213). That
|
|
481
|
+
// value is already fully re-evaluated and spliced via `__bfSlot`
|
|
482
|
+
// whenever insert() (re-)mounts the branch; wrapping it in an
|
|
483
|
+
// *additional* createEffect re-invokes the callback (producing a
|
|
484
|
+
// second, independent live element) on the very first run, and the
|
|
485
|
+
// loop-child arm's `$t()`-based anchor lookup can't cleanly displace an
|
|
486
|
+
// already-mounted Element — so the second instance renders beside the
|
|
487
|
+
// first instead of replacing it (surfaced via barefootjs-xyflow's
|
|
488
|
+
// renderNode reference page in CI).
|
|
489
|
+
const source = `
|
|
490
|
+
'use client'
|
|
491
|
+
import { createSignal } from '@barefootjs/client'
|
|
492
|
+
|
|
493
|
+
interface Node { id: string }
|
|
494
|
+
|
|
495
|
+
export function Flow(props: { renderNode?: (n: Node) => unknown }) {
|
|
496
|
+
const [nodes] = createSignal<Node[]>([])
|
|
497
|
+
return (
|
|
498
|
+
<div>
|
|
499
|
+
{nodes().map(n => (
|
|
500
|
+
<div key={n.id}>
|
|
501
|
+
{props.renderNode ? props.renderNode(n) : <span>{n.id}</span>}
|
|
502
|
+
</div>
|
|
503
|
+
))}
|
|
504
|
+
</div>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
`
|
|
508
|
+
const result = compileJSX(source, 'Flow.tsx', { adapter })
|
|
509
|
+
expect(result.errors).toHaveLength(0)
|
|
510
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
511
|
+
|
|
512
|
+
// No nested createEffect re-invoking `props.renderNode(n)` — the
|
|
513
|
+
// conditional's own template()/insert() already owns this value.
|
|
514
|
+
expect(content).not.toMatch(/createEffect\(\(\) => \{ __rt_\w+ = __bfText\(__rt_\w+, \(?\s*props\.renderNode\(n\(\)\)/)
|
|
515
|
+
})
|
|
516
|
+
})
|
|
@@ -111,8 +111,11 @@ describe('nested loop binding ids (#1795 Phase 3)', () => {
|
|
|
111
111
|
|
|
112
112
|
test('loop-child conditional insert() and its branch text carry ids', () => {
|
|
113
113
|
const on = clientJs(nestedSource, 'Nested', true)
|
|
114
|
-
// The branch-arm text (`{r.label}` inside the conditional's true arm)
|
|
115
|
-
|
|
114
|
+
// The branch-arm text (`{r.label}` inside the conditional's true arm) —
|
|
115
|
+
// __bfText (not a naive `.textContent = String(...)`) so a Child-position
|
|
116
|
+
// expression whose value is a live Node splices by identity instead of
|
|
117
|
+
// stringifying (#2347).
|
|
118
|
+
expect(on).toMatch(/__rt_s\d+ = __bfText\(__rt_s\d+, r\(\)\.label\) }, "Nested#binding:s\d+"\)/)
|
|
116
119
|
// The inner loop's child text (`{t}`).
|
|
117
120
|
expect(on).toMatch(/String\(t\(\)\) }, "Nested#binding:s\d+"\)/)
|
|
118
121
|
})
|