@barefootjs/jsx 0.3.0 → 0.5.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/interface.d.ts +20 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +36 -19
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/import-map.d.ts +56 -0
- package/dist/import-map.d.ts.map +1 -0
- package/dist/import-map.js +18 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +160 -162
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/scanner/js-scanner.d.ts +10 -0
- package/dist/scanner/js-scanner.d.ts.map +1 -1
- package/dist/scanner/js-scanner.js +5 -0
- package/package.json +7 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +134 -8
- package/src/__tests__/auto-defer-brand.test.ts +81 -0
- package/src/__tests__/child-components-in-map.test.ts +76 -0
- package/src/__tests__/import-map.test.ts +75 -0
- package/src/__tests__/ir-sort-comparator.test.ts +212 -9
- package/src/__tests__/token-contains-ident.test.ts +27 -0
- package/src/__tests__/unsupported-expression.test.ts +42 -13
- package/src/adapters/interface.ts +20 -0
- package/src/expression-parser.ts +265 -50
- package/src/import-map.ts +72 -0
- package/src/index.ts +5 -1
- package/src/ir-to-client-js/html-template.ts +17 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +8 -4
- package/src/ir-to-client-js/utils.ts +29 -115
- package/src/scanner/js-scanner.ts +16 -1
|
@@ -474,7 +474,133 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
|
|
|
474
474
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
475
475
|
`;
|
|
476
476
|
|
|
477
|
-
exports[`docs/core/rendering/jsx-compatibility.md doc-examples
|
|
477
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L69 — ✅ Multi-key: sort by price, break ties by name 1`] = `
|
|
478
|
+
"import { $, $t, createComponent, createEffect, createSignal, hydrate, initChild, mapArray, renderChild } from '@barefootjs/client/runtime'
|
|
479
|
+
|
|
480
|
+
export function initTodoItem(__scope, _p = {}) {
|
|
481
|
+
if (!__scope) return
|
|
482
|
+
const __scopeId = __scope.getAttribute('bf-s')
|
|
483
|
+
|
|
484
|
+
const [_s0] = $t(__scope, 's0')
|
|
485
|
+
|
|
486
|
+
createEffect(() => {
|
|
487
|
+
const __val = String(_p.todo)
|
|
488
|
+
if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
hydrate('TodoItem__b3c36eee', { init: initTodoItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.todo)}<!--/--></li>\` })
|
|
494
|
+
export function TodoItem(_p, __bfKey) { return createComponent('TodoItem__b3c36eee', _p, __bfKey) }
|
|
495
|
+
export function initItem(__scope, _p = {}) {
|
|
496
|
+
if (!__scope) return
|
|
497
|
+
const __scopeId = __scope.getAttribute('bf-s')
|
|
498
|
+
|
|
499
|
+
const [_s0] = $t(__scope, 's0')
|
|
500
|
+
|
|
501
|
+
createEffect(() => {
|
|
502
|
+
const __val = String(_p.item)
|
|
503
|
+
if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
hydrate('Item__b3c36eee', { init: initItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.item)}<!--/--></li>\` })
|
|
509
|
+
export function Item(_p, __bfKey) { return createComponent('Item__b3c36eee', _p, __bfKey) }
|
|
510
|
+
function initDashboard() {}
|
|
511
|
+
|
|
512
|
+
hydrate('Dashboard__b3c36eee', { init: initDashboard, template: (_p) => \`<div>D</div>\` })
|
|
513
|
+
export function Dashboard(_p, __bfKey) { return createComponent('Dashboard__b3c36eee', _p, __bfKey) }
|
|
514
|
+
export function initExample(__scope, _p = {}) {
|
|
515
|
+
if (!__scope) return
|
|
516
|
+
const __scopeId = __scope.getAttribute('bf-s')
|
|
517
|
+
|
|
518
|
+
const children = _p.children
|
|
519
|
+
const [count, setCount] = createSignal(0)
|
|
520
|
+
const [isLoggedIn] = createSignal(false)
|
|
521
|
+
const [todos] = createSignal([])
|
|
522
|
+
const [items] = createSignal([])
|
|
523
|
+
const [filter] = createSignal('all')
|
|
524
|
+
const [accepted] = createSignal(false)
|
|
525
|
+
const [text, setText] = createSignal('')
|
|
526
|
+
|
|
527
|
+
const [_s1] = $(__scope, 's1')
|
|
528
|
+
|
|
529
|
+
mapArray(() => items().toSorted((a, b) => a.price - b.price || a.name.localeCompare(b.name)), _s1, (item) => String(item.id), (item, __idx, __existing) => {
|
|
530
|
+
if (__existing) { initChild('Item__b3c36eee', __existing, { get item() { return item() } }); return __existing }
|
|
531
|
+
return createComponent('Item__b3c36eee', { get item() { return item() } }, item().id)
|
|
532
|
+
}, 'l0')
|
|
533
|
+
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).toSorted((a, b) => a.price - b.price || a.name.localeCompare(b.name)).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
|
|
537
|
+
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
538
|
+
`;
|
|
539
|
+
|
|
540
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L74 — ✅ Relational ternary 1`] = `
|
|
541
|
+
"import { $, $t, createComponent, createEffect, createSignal, hydrate, initChild, mapArray, renderChild } from '@barefootjs/client/runtime'
|
|
542
|
+
|
|
543
|
+
export function initTodoItem(__scope, _p = {}) {
|
|
544
|
+
if (!__scope) return
|
|
545
|
+
const __scopeId = __scope.getAttribute('bf-s')
|
|
546
|
+
|
|
547
|
+
const [_s0] = $t(__scope, 's0')
|
|
548
|
+
|
|
549
|
+
createEffect(() => {
|
|
550
|
+
const __val = String(_p.todo)
|
|
551
|
+
if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
hydrate('TodoItem__b3c36eee', { init: initTodoItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.todo)}<!--/--></li>\` })
|
|
557
|
+
export function TodoItem(_p, __bfKey) { return createComponent('TodoItem__b3c36eee', _p, __bfKey) }
|
|
558
|
+
export function initItem(__scope, _p = {}) {
|
|
559
|
+
if (!__scope) return
|
|
560
|
+
const __scopeId = __scope.getAttribute('bf-s')
|
|
561
|
+
|
|
562
|
+
const [_s0] = $t(__scope, 's0')
|
|
563
|
+
|
|
564
|
+
createEffect(() => {
|
|
565
|
+
const __val = String(_p.item)
|
|
566
|
+
if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
hydrate('Item__b3c36eee', { init: initItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.item)}<!--/--></li>\` })
|
|
572
|
+
export function Item(_p, __bfKey) { return createComponent('Item__b3c36eee', _p, __bfKey) }
|
|
573
|
+
function initDashboard() {}
|
|
574
|
+
|
|
575
|
+
hydrate('Dashboard__b3c36eee', { init: initDashboard, template: (_p) => \`<div>D</div>\` })
|
|
576
|
+
export function Dashboard(_p, __bfKey) { return createComponent('Dashboard__b3c36eee', _p, __bfKey) }
|
|
577
|
+
export function initExample(__scope, _p = {}) {
|
|
578
|
+
if (!__scope) return
|
|
579
|
+
const __scopeId = __scope.getAttribute('bf-s')
|
|
580
|
+
|
|
581
|
+
const children = _p.children
|
|
582
|
+
const [count, setCount] = createSignal(0)
|
|
583
|
+
const [isLoggedIn] = createSignal(false)
|
|
584
|
+
const [todos] = createSignal([])
|
|
585
|
+
const [items] = createSignal([])
|
|
586
|
+
const [filter] = createSignal('all')
|
|
587
|
+
const [accepted] = createSignal(false)
|
|
588
|
+
const [text, setText] = createSignal('')
|
|
589
|
+
|
|
590
|
+
const [_s1] = $(__scope, 's1')
|
|
591
|
+
|
|
592
|
+
mapArray(() => items().toSorted((a, b) => a.price > b.price ? 1 : -1), _s1, (item) => String(item.id), (item, __idx, __existing) => {
|
|
593
|
+
if (__existing) { initChild('Item__b3c36eee', __existing, { get item() { return item() } }); return __existing }
|
|
594
|
+
return createComponent('Item__b3c36eee', { get item() { return item() } }, item().id)
|
|
595
|
+
}, 'l0')
|
|
596
|
+
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).toSorted((a, b) => a.price > b.price ? 1 : -1).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
|
|
600
|
+
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
601
|
+
`;
|
|
602
|
+
|
|
603
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L85 — (no label) 1`] = `
|
|
478
604
|
"import { $, $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
|
|
479
605
|
|
|
480
606
|
export function initTodoItem(__scope, _p = {}) {
|
|
@@ -536,7 +662,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div><button bf="s0"
|
|
|
536
662
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
537
663
|
`;
|
|
538
664
|
|
|
539
|
-
exports[`docs/core/rendering/jsx-compatibility.md doc-examples
|
|
665
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L131 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
|
|
540
666
|
"import { $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
|
|
541
667
|
|
|
542
668
|
export function initTodoItem(__scope, _p = {}) {
|
|
@@ -599,7 +725,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
|
|
|
599
725
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
600
726
|
`;
|
|
601
727
|
|
|
602
|
-
exports[`docs/core/rendering/jsx-compatibility.md doc-examples
|
|
728
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L134 — ✅ Use /* @client */ 1`] = `
|
|
603
729
|
"import { $t, createComponent, createEffect, createSignal, hydrate, updateClientMarker } from '@barefootjs/client/runtime'
|
|
604
730
|
|
|
605
731
|
export function initTodoItem(__scope, _p = {}) {
|
|
@@ -660,7 +786,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
|
|
|
660
786
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
661
787
|
`;
|
|
662
788
|
|
|
663
|
-
exports[`docs/core/rendering/jsx-compatibility.md doc-examples
|
|
789
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L151 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
|
|
664
790
|
"import { $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
|
|
665
791
|
|
|
666
792
|
export function initTodoItem(__scope, _p = {}) {
|
|
@@ -723,7 +849,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
|
|
|
723
849
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
724
850
|
`;
|
|
725
851
|
|
|
726
|
-
exports[`docs/core/rendering/jsx-compatibility.md doc-examples
|
|
852
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L154 — ✅ Use arrow functions for adapter portability 1`] = `
|
|
727
853
|
"import { $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
|
|
728
854
|
|
|
729
855
|
export function initTodoItem(__scope, _p = {}) {
|
|
@@ -786,7 +912,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
|
|
|
786
912
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
787
913
|
`;
|
|
788
914
|
|
|
789
|
-
exports[`docs/core/rendering/jsx-compatibility.md doc-examples
|
|
915
|
+
exports[`docs/core/rendering/jsx-compatibility.md doc-examples L168 — ✅ Use /* @client */ 1`] = `
|
|
790
916
|
"import { $, $t, createComponent, createEffect, createSignal, hydrate, initChild, mapArray, renderChild } from '@barefootjs/client/runtime'
|
|
791
917
|
|
|
792
918
|
export function initTodoItem(__scope, _p = {}) {
|
|
@@ -838,14 +964,14 @@ export function initExample(__scope, _p = {}) {
|
|
|
838
964
|
|
|
839
965
|
const [_s1] = $(__scope, 's1')
|
|
840
966
|
|
|
841
|
-
mapArray(() => items().sort((a, b) => a.name > b.name ? 1 : -1), _s1, (item) => String(item.id), (item, __idx, __existing) => {
|
|
967
|
+
mapArray(() => items().sort((a, b) => { const an = a.name; return an > b.name ? 1 : -1 }), _s1, (item) => String(item.id), (item, __idx, __existing) => {
|
|
842
968
|
if (__existing) { initChild('Item__b3c36eee', __existing, { get item() { return item() } }); return __existing }
|
|
843
969
|
return createComponent('Item__b3c36eee', { get item() { return item() } }, item().id)
|
|
844
970
|
}, 'l0')
|
|
845
971
|
|
|
846
972
|
}
|
|
847
973
|
|
|
848
|
-
hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).sort((a, b) => a.name > b.name ? 1 : -1).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
|
|
974
|
+
hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).sort((a, b) => { const an = a.name; return an > b.name ? 1 : -1 }).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
|
|
849
975
|
export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
|
|
850
976
|
`;
|
|
851
977
|
|
|
@@ -201,3 +201,84 @@ describe('auto-defer brand-package reactive bindings (#1638)', () => {
|
|
|
201
201
|
expect(templateBody).toContain('data-count=')
|
|
202
202
|
})
|
|
203
203
|
})
|
|
204
|
+
|
|
205
|
+
describe('client hydrate template defers brand conditionals (#1645)', () => {
|
|
206
|
+
// The `template: (_p) => ...` lambda runs at module scope when the
|
|
207
|
+
// component is client-rendered via `createComponent` (not when hydrating
|
|
208
|
+
// existing SSR DOM). It cannot reproduce per-instance `createForm` state,
|
|
209
|
+
// so an auto-deferred conditional must emit empty cond markers — exactly
|
|
210
|
+
// like the SSR adapter — and let `init`'s `insert()` populate the branch.
|
|
211
|
+
|
|
212
|
+
test('non-inlinable createForm (onSubmit): no undefined.field, emits cond markers', () => {
|
|
213
|
+
const { templateBody } = compileWithBrand(`
|
|
214
|
+
'use client'
|
|
215
|
+
import { createForm } from './_form-defs'
|
|
216
|
+
|
|
217
|
+
export function SignupForm() {
|
|
218
|
+
const form = createForm({
|
|
219
|
+
onSubmit: async (data) => { await fetch('/signup', { method: 'POST', body: JSON.stringify(data) }) },
|
|
220
|
+
})
|
|
221
|
+
const email = form.field('email')
|
|
222
|
+
return (
|
|
223
|
+
<form>
|
|
224
|
+
<input value={email.value()} />
|
|
225
|
+
{email.error() && <p>{email.error()}</p>}
|
|
226
|
+
</form>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
`)
|
|
230
|
+
|
|
231
|
+
// Never re-derive the form at module scope: `undefined.field(...)` throws.
|
|
232
|
+
expect(templateBody).not.toContain('undefined.field')
|
|
233
|
+
expect(templateBody).not.toContain('.field(')
|
|
234
|
+
// The deferred conditional collapses to the same empty markers SSR emits.
|
|
235
|
+
expect(templateBody).toMatch(/<!--bf-cond-start:s\d+--><!--bf-cond-end:s\d+-->/)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('inlinable createForm (no onSubmit): no re-inlined createForm in template', () => {
|
|
239
|
+
const { templateBody } = compileWithBrand(`
|
|
240
|
+
'use client'
|
|
241
|
+
import { createForm } from './_form-defs'
|
|
242
|
+
|
|
243
|
+
export function SignupForm() {
|
|
244
|
+
const form = createForm({ defaultValues: { email: '' } })
|
|
245
|
+
const email = form.field('email')
|
|
246
|
+
return (
|
|
247
|
+
<form>
|
|
248
|
+
<input value={email.value()} />
|
|
249
|
+
{email.error() && <p>{email.error()}</p>}
|
|
250
|
+
</form>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
`)
|
|
254
|
+
|
|
255
|
+
// A re-inlined `createForm({...})` would build a throwaway instance on
|
|
256
|
+
// every template render (error always '', never the live instance).
|
|
257
|
+
expect(templateBody).not.toContain('createForm(')
|
|
258
|
+
expect(templateBody).not.toContain('undefined.field')
|
|
259
|
+
expect(templateBody).toMatch(/<!--bf-cond-start:s\d+--><!--bf-cond-end:s\d+-->/)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('init still wires the deferred conditional via insert()', () => {
|
|
263
|
+
const { initBody } = compileWithBrand(`
|
|
264
|
+
'use client'
|
|
265
|
+
import { createForm } from './_form-defs'
|
|
266
|
+
|
|
267
|
+
export function SignupForm() {
|
|
268
|
+
const form = createForm()
|
|
269
|
+
const email = form.field('email')
|
|
270
|
+
return (
|
|
271
|
+
<form>
|
|
272
|
+
<input value={email.value()} />
|
|
273
|
+
{email.error() && <p>{email.error()}</p>}
|
|
274
|
+
</form>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
`)
|
|
278
|
+
|
|
279
|
+
// The reactive binding lives in init (where `email`/`form` are in scope),
|
|
280
|
+
// not in the module-scope template lambda.
|
|
281
|
+
expect(initBody).toMatch(/insert\(/)
|
|
282
|
+
expect(initBody).toContain('email.error()')
|
|
283
|
+
})
|
|
284
|
+
})
|
|
@@ -729,4 +729,80 @@ describe('child components inside .map() (#344)', () => {
|
|
|
729
729
|
expect(content).toContain('children[__idx]')
|
|
730
730
|
expect(content).not.toContain('children[__idx + ')
|
|
731
731
|
})
|
|
732
|
+
|
|
733
|
+
test('nested .map() with multiple inner components emits unique __compEl bindings (#1664)', () => {
|
|
734
|
+
const source = `
|
|
735
|
+
'use client'
|
|
736
|
+
|
|
737
|
+
export function Picker() {
|
|
738
|
+
const GROUPS = [
|
|
739
|
+
{ id: 'a', items: [{ id: 'x', label: 'X' }] },
|
|
740
|
+
]
|
|
741
|
+
return (
|
|
742
|
+
<div>
|
|
743
|
+
{GROUPS.map(group => (
|
|
744
|
+
<div key={group.id}>
|
|
745
|
+
{group.items.map(it => (
|
|
746
|
+
<div key={it.id}>
|
|
747
|
+
<SelectItem value={it.id}>{it.label}</SelectItem>
|
|
748
|
+
<SelectIcon name={it.id} />
|
|
749
|
+
</div>
|
|
750
|
+
))}
|
|
751
|
+
</div>
|
|
752
|
+
))}
|
|
753
|
+
</div>
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
`
|
|
757
|
+
const result = compileJSX(source, 'Picker.tsx', { adapter })
|
|
758
|
+
expect(result.errors).toHaveLength(0)
|
|
759
|
+
|
|
760
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
761
|
+
expect(clientJs).toBeDefined()
|
|
762
|
+
const content = clientJs!.content
|
|
763
|
+
|
|
764
|
+
// Both inner-loop components must be initialised.
|
|
765
|
+
expect(content).toContain("initChild('SelectItem'")
|
|
766
|
+
expect(content).toContain("initChild('SelectIcon'")
|
|
767
|
+
|
|
768
|
+
// No re-declaration of `__compEl` in the shared inner-forEach scope:
|
|
769
|
+
// each comp must use a uniquely-suffixed binding.
|
|
770
|
+
expect(content).toContain('__compEl0')
|
|
771
|
+
expect(content).toContain('__compEl1')
|
|
772
|
+
|
|
773
|
+
// The bug threw "Identifier '__compEl' has already been declared" — the
|
|
774
|
+
// unsuffixed binding must not appear when multiple comps share a scope.
|
|
775
|
+
expect(content).not.toContain('const __compEl =')
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
test('nested .map() with a single inner component keeps the plain __compEl binding (#1664)', () => {
|
|
779
|
+
const source = `
|
|
780
|
+
'use client'
|
|
781
|
+
|
|
782
|
+
export function Picker() {
|
|
783
|
+
const GROUPS = [
|
|
784
|
+
{ id: 'a', items: [{ id: 'x', label: 'X' }] },
|
|
785
|
+
]
|
|
786
|
+
return (
|
|
787
|
+
<div>
|
|
788
|
+
{GROUPS.map(group => (
|
|
789
|
+
<div key={group.id}>
|
|
790
|
+
{group.items.map(it => (
|
|
791
|
+
<SelectItem key={it.id} value={it.id}>{it.label}</SelectItem>
|
|
792
|
+
))}
|
|
793
|
+
</div>
|
|
794
|
+
))}
|
|
795
|
+
</div>
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
`
|
|
799
|
+
const result = compileJSX(source, 'Picker.tsx', { adapter })
|
|
800
|
+
expect(result.errors).toHaveLength(0)
|
|
801
|
+
|
|
802
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
803
|
+
expect(content).toContain("initChild('SelectItem'")
|
|
804
|
+
// Single comp keeps the unsuffixed name.
|
|
805
|
+
expect(content).toContain('const __compEl =')
|
|
806
|
+
expect(content).not.toContain('__compEl0')
|
|
807
|
+
})
|
|
732
808
|
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderImportMapHtml tests
|
|
3
|
+
*
|
|
4
|
+
* The shared importmap-snippet renderer turns a parsed `barefoot-externals.json`
|
|
5
|
+
* into the `<script type="importmap">` (+ `<link rel="modulepreload">`) HTML that
|
|
6
|
+
* `bf build` emits as `barefoot-importmap.html` for template-string adapters
|
|
7
|
+
* (issue #1644). This is the single source of truth for that snippet.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { renderImportMapHtml } from '../import-map'
|
|
11
|
+
|
|
12
|
+
function parseImportMap(html: string): Record<string, string> {
|
|
13
|
+
const match = html.match(/<script type="importmap">(.*?)<\/script>/s)
|
|
14
|
+
if (!match) throw new Error(`no importmap in: ${html}`)
|
|
15
|
+
// Decode the < escape the renderer applies before parsing.
|
|
16
|
+
return JSON.parse(match[1]).imports
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('renderImportMapHtml', () => {
|
|
20
|
+
test('emits the manifest importmap imports verbatim', () => {
|
|
21
|
+
const html = renderImportMapHtml({
|
|
22
|
+
importmap: {
|
|
23
|
+
imports: {
|
|
24
|
+
'@barefootjs/client': '/components/barefoot.js',
|
|
25
|
+
'@barefootjs/client/runtime': '/components/barefoot.js',
|
|
26
|
+
zod: 'https://esm.sh/zod@4.4.3',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
preloads: [],
|
|
30
|
+
})
|
|
31
|
+
expect(parseImportMap(html)).toEqual({
|
|
32
|
+
'@barefootjs/client': '/components/barefoot.js',
|
|
33
|
+
'@barefootjs/client/runtime': '/components/barefoot.js',
|
|
34
|
+
zod: 'https://esm.sh/zod@4.4.3',
|
|
35
|
+
})
|
|
36
|
+
expect(html).not.toContain('modulepreload')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('emits modulepreload links with crossorigin for manifest preloads (#1648)', () => {
|
|
40
|
+
const html = renderImportMapHtml({
|
|
41
|
+
importmap: { imports: {} },
|
|
42
|
+
preloads: ['/components/form.js', 'https://esm.sh/zod@4.4.3'],
|
|
43
|
+
})
|
|
44
|
+
expect(html).toContain('<link rel="modulepreload" href="/components/form.js" crossorigin>')
|
|
45
|
+
expect(html).toContain('<link rel="modulepreload" href="https://esm.sh/zod@4.4.3" crossorigin>')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('reads defensively from a partial manifest', () => {
|
|
49
|
+
expect(parseImportMap(renderImportMapHtml({}))).toEqual({})
|
|
50
|
+
expect(renderImportMapHtml({})).not.toContain('modulepreload')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('ends with a trailing newline (template-include friendly)', () => {
|
|
54
|
+
expect(renderImportMapHtml({ importmap: { imports: {} } }).endsWith('\n')).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('escapes < in the importmap JSON so a URL cannot break out of the script', () => {
|
|
58
|
+
const html = renderImportMapHtml({
|
|
59
|
+
importmap: { imports: { evil: 'https://x/</script><script>alert(1)</script>' } },
|
|
60
|
+
})
|
|
61
|
+
// The literal closing tag must not appear before the importmap's own.
|
|
62
|
+
const importmapClose = html.indexOf('</script>')
|
|
63
|
+
expect(html.slice(0, importmapClose)).not.toContain('</script>')
|
|
64
|
+
// But the value still round-trips through JSON.parse.
|
|
65
|
+
expect(parseImportMap(html).evil).toBe('https://x/</script><script>alert(1)</script>')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('escapes double quotes and angle brackets in preload hrefs', () => {
|
|
69
|
+
const html = renderImportMapHtml({
|
|
70
|
+
preloads: ['/components/"onerror=alert(1).js'],
|
|
71
|
+
})
|
|
72
|
+
expect(html).not.toContain('"onerror=alert(1)')
|
|
73
|
+
expect(html).toContain('"onerror=alert(1)')
|
|
74
|
+
})
|
|
75
|
+
})
|