@barefootjs/jsx 0.4.0 → 0.5.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/interface.d.ts +20 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/test-adapter.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 +333 -199
- package/dist/ir-to-client-js/collect-elements.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 +14 -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/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +0 -14
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +7 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +2 -2
- 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/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +438 -190
- package/src/__tests__/adapter-output.test.ts +49 -0
- package/src/__tests__/child-components-in-map.test.ts +76 -0
- package/src/__tests__/client-js-generation.test.ts +5 -2
- package/src/__tests__/import-map.test.ts +75 -0
- package/src/__tests__/inline-jsx-callback.test.ts +95 -0
- package/src/__tests__/ir-jsx-props.test.ts +5 -2
- package/src/__tests__/ir-sort-comparator.test.ts +212 -9
- package/src/__tests__/loop-item-conditional-codegen.test.ts +81 -0
- package/src/__tests__/map-logical-jsx-helper.test.ts +159 -0
- package/src/__tests__/missing-key-in-list.test.ts +49 -0
- package/src/__tests__/reactive-attrs-in-map.test.ts +41 -0
- 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/adapters/test-adapter.ts +16 -1
- 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/collect-elements.ts +3 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +17 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +14 -0
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +7 -2
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +60 -0
- package/src/ir-to-client-js/emit-reactive.ts +12 -3
- package/src/ir-to-client-js/html-template.ts +29 -3
- package/src/ir-to-client-js/imports.ts +2 -2
- package/src/ir-to-client-js/reactivity.ts +17 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +8 -4
- package/src/ir-to-client-js/types.ts +7 -0
- package/src/ir-to-client-js/utils.ts +31 -116
- package/src/jsx-to-ir.ts +161 -12
- package/src/preprocess-inline-jsx-callbacks.ts +28 -10
- package/src/scanner/js-scanner.ts +16 -1
- package/src/types.ts +12 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS Compiler — `.map()` callback whose body is a logical
|
|
3
|
+
* expression (`&&` / `||` / `??`) that renders JSX.
|
|
4
|
+
*
|
|
5
|
+
* Regression for #1665: calling a module-level JSX helper from inside a
|
|
6
|
+
* reactive `.map()` child (`THEMES.map(t => sel === t.id && themeLogo(t.id))`)
|
|
7
|
+
* threw `ReferenceError: themeLogo is not defined` at hydration.
|
|
8
|
+
*
|
|
9
|
+
* Root cause: `transformMapCall` only recognised JSX-element, ternary,
|
|
10
|
+
* parenthesized, flatMap-array, and block callback bodies. A logical-`&&`
|
|
11
|
+
* body fell through, leaving `children` empty, so the whole `.map(...)`
|
|
12
|
+
* was emitted verbatim as a reactive-text expression — the inline JSX was
|
|
13
|
+
* never compiled and the JSX helper was never inlined nor declared,
|
|
14
|
+
* producing a ReferenceError.
|
|
15
|
+
*
|
|
16
|
+
* The fix routes a JSX-rendering logical callback body through the shared
|
|
17
|
+
* JSX expression transformer, which lowers it into an IRConditional and
|
|
18
|
+
* inlines any JSX helper (the same path the ternary form already used).
|
|
19
|
+
* The routing is scoped to logical operators so the bare-call body
|
|
20
|
+
* (`map(t => renderItem(t))`, owned by #546) and scalar logical bodies
|
|
21
|
+
* (`t.active && t.label`) keep their existing reactive-text behaviour.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, test, expect } from 'bun:test'
|
|
25
|
+
import { compileJSX } from '../compiler'
|
|
26
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
27
|
+
|
|
28
|
+
const adapter = new TestAdapter()
|
|
29
|
+
|
|
30
|
+
describe('.map() with logical / JSX-helper-call body (#1665)', () => {
|
|
31
|
+
test('module-level JSX helper in a `&&` map body is inlined, not left as a bare call', () => {
|
|
32
|
+
const source = `
|
|
33
|
+
'use client'
|
|
34
|
+
const THEMES = [{ id: 'piconic' }, { id: 'hono' }]
|
|
35
|
+
function themeLogo(id: string) {
|
|
36
|
+
if (id === 'hono') return <span>hono</span>
|
|
37
|
+
return <span>piconic</span>
|
|
38
|
+
}
|
|
39
|
+
export function Header(props: { sel: string }) {
|
|
40
|
+
return <div>{THEMES.map(t => props.sel === t.id && themeLogo(t.id))}</div>
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
const result = compileJSX(source, 'Header.tsx', { adapter })
|
|
44
|
+
expect(result.errors).toHaveLength(0)
|
|
45
|
+
|
|
46
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
47
|
+
|
|
48
|
+
// The helper must NOT survive as a runtime call in the client bundle —
|
|
49
|
+
// it is inlined. A surviving `themeLogo(` call with no declaration is
|
|
50
|
+
// the exact ReferenceError the issue reported.
|
|
51
|
+
expect(clientJs).not.toMatch(/themeLogo\s*\(/)
|
|
52
|
+
|
|
53
|
+
// The map is compiled into a loop, and the helper's JSX is inlined.
|
|
54
|
+
expect(clientJs).toContain('<!--bf-loop')
|
|
55
|
+
expect(clientJs).toContain('hono')
|
|
56
|
+
expect(clientJs).toContain('piconic')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('parenthesized `&&` with a single-return JSX helper is inlined', () => {
|
|
60
|
+
const source = `
|
|
61
|
+
'use client'
|
|
62
|
+
const THEMES = [{ id: 'piconic' }, { id: 'hono' }]
|
|
63
|
+
function themeLogo(id: string) { return <span>{id}</span> }
|
|
64
|
+
export function Header(props: { sel: string }) {
|
|
65
|
+
return <div>{THEMES.map(t => (props.sel === t.id && themeLogo(t.id)))}</div>
|
|
66
|
+
}
|
|
67
|
+
`
|
|
68
|
+
const result = compileJSX(source, 'Header.tsx', { adapter })
|
|
69
|
+
expect(result.errors).toHaveLength(0)
|
|
70
|
+
|
|
71
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
72
|
+
|
|
73
|
+
expect(clientJs).not.toMatch(/themeLogo\s*\(/)
|
|
74
|
+
expect(clientJs).toContain('<!--bf-loop')
|
|
75
|
+
expect(clientJs).toContain('<span')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('`??` JSX-helper fallback in a map body is inlined', () => {
|
|
79
|
+
const source = `
|
|
80
|
+
'use client'
|
|
81
|
+
const THEMES = [{ id: 'piconic' }, { id: 'hono' }]
|
|
82
|
+
function themeLogo(id: string) { return <span>{id}</span> }
|
|
83
|
+
export function Header(props: { current?: string }) {
|
|
84
|
+
return <div>{THEMES.map(t => props.current ?? themeLogo(t.id))}</div>
|
|
85
|
+
}
|
|
86
|
+
`
|
|
87
|
+
const result = compileJSX(source, 'Header.tsx', { adapter })
|
|
88
|
+
expect(result.errors).toHaveLength(0)
|
|
89
|
+
|
|
90
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
91
|
+
|
|
92
|
+
// `transformJsxExpression` now routes a `||`/`??` whose right operand
|
|
93
|
+
// renders JSX via a tracked helper through the nullish transformer, so
|
|
94
|
+
// the helper is inlined rather than left as a verbatim call.
|
|
95
|
+
expect(clientJs).not.toMatch(/themeLogo\s*\(/)
|
|
96
|
+
expect(clientJs).toContain('<span')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('bare JSX-helper call body keeps its #546 reactive-text behaviour', () => {
|
|
100
|
+
// Boundary guard: the narrowed fix must NOT re-route a bare call body
|
|
101
|
+
// into the conditional path — that remains #546 territory. A local
|
|
102
|
+
// arrow helper is declared in init scope, so the call survives.
|
|
103
|
+
const source = `
|
|
104
|
+
'use client'
|
|
105
|
+
import { createSignal } from '@barefootjs/client'
|
|
106
|
+
export function List() {
|
|
107
|
+
const [items, _set] = createSignal([{ id: '1' }])
|
|
108
|
+
const renderItem = (item: any) => <li>{item.id}</li>
|
|
109
|
+
return <ul>{items().map(item => renderItem(item))}</ul>
|
|
110
|
+
}
|
|
111
|
+
`
|
|
112
|
+
const result = compileJSX(source, 'List.tsx', { adapter })
|
|
113
|
+
expect(result.errors).toHaveLength(0)
|
|
114
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
115
|
+
// The bare call is preserved (not inlined into a conditional loop).
|
|
116
|
+
expect(clientJs).toMatch(/renderItem\s*\(/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('inline JSX in a `&&` map body compiles instead of emitting raw JSX verbatim', () => {
|
|
120
|
+
// `key` is required on the `&&` JSX operand (#1665 / BF023): a whole-item
|
|
121
|
+
// conditional renders 0-or-1 element per iteration and needs a stable key
|
|
122
|
+
// for correct reconciliation, exactly like a ternary branch.
|
|
123
|
+
const source = `
|
|
124
|
+
'use client'
|
|
125
|
+
const THEMES = [{ id: 'piconic' }, { id: 'hono' }]
|
|
126
|
+
export function Header(props: { sel: string }) {
|
|
127
|
+
return <div>{THEMES.map(t => props.sel === t.id && <span key={t.id}>{t.id}</span>)}</div>
|
|
128
|
+
}
|
|
129
|
+
`
|
|
130
|
+
const result = compileJSX(source, 'Header.tsx', { adapter })
|
|
131
|
+
expect(result.errors).toHaveLength(0)
|
|
132
|
+
|
|
133
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
134
|
+
// Compiled into a loop with a conditional branch — not the raw,
|
|
135
|
+
// un-compiled `<span>{t.id}</span>` JSX literal the buggy path left
|
|
136
|
+
// inside the client-bundle template string.
|
|
137
|
+
expect(clientJs).toContain('<!--bf-loop')
|
|
138
|
+
expect(clientJs).not.toContain('<span>{t.id}</span>')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('scalar `&&` map body keeps its plain reactive-text behaviour', () => {
|
|
142
|
+
// Guard: a non-JSX logical body must NOT be re-routed into the
|
|
143
|
+
// conditional-control-flow path. It stays a verbatim map expression.
|
|
144
|
+
const source = `
|
|
145
|
+
'use client'
|
|
146
|
+
import { createSignal } from '@barefootjs/client'
|
|
147
|
+
export function Labels() {
|
|
148
|
+
const [items, _set] = createSignal([{ active: true, name: 'A' }])
|
|
149
|
+
return <div>{items().map(t => t.active && t.name)}</div>
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
const result = compileJSX(source, 'Labels.tsx', { adapter })
|
|
153
|
+
expect(result.errors).toHaveLength(0)
|
|
154
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
155
|
+
// Still rendered as the raw map expression (no loop control flow).
|
|
156
|
+
expect(clientJs).not.toContain('<!--bf-loop')
|
|
157
|
+
expect(clientJs).toContain('.map(')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -224,6 +224,55 @@ describe('BF023 — ternary .map() callback', () => {
|
|
|
224
224
|
})
|
|
225
225
|
})
|
|
226
226
|
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// BF023 — logical (&&, ||, ??) .map() callback (#1665)
|
|
229
|
+
//
|
|
230
|
+
// A whole-item conditional (`cond && <li>`) renders 0-or-1 element per
|
|
231
|
+
// iteration. The same key reconciliation applies — without a key, toggling
|
|
232
|
+
// items collapses or misorders them — so the JSX element inside a logical
|
|
233
|
+
// expression must carry a key, just like a ternary branch.
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
describe('BF023 — logical && / || .map() callback (#1665)', () => {
|
|
237
|
+
test('&& body without key raises BF023', () => {
|
|
238
|
+
const source = `
|
|
239
|
+
interface Item { active: boolean; id: string }
|
|
240
|
+
export function List({ items }: { items: Item[] }) {
|
|
241
|
+
return <ul>{items.map(item => item.active && <li>{item.id}</li>)}</ul>
|
|
242
|
+
}
|
|
243
|
+
`
|
|
244
|
+
const errs = errorsFor(ErrorCodes.MISSING_KEY_IN_LIST, source)
|
|
245
|
+
expect(errs).toHaveLength(1)
|
|
246
|
+
expect(errs[0].severity).toBe('error')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('&& body with key — no error', () => {
|
|
250
|
+
const source = `
|
|
251
|
+
interface Item { active: boolean; id: string }
|
|
252
|
+
export function List({ items }: { items: Item[] }) {
|
|
253
|
+
return <ul>{items.map(item => item.active && <li key={item.id}>{item.id}</li>)}</ul>
|
|
254
|
+
}
|
|
255
|
+
`
|
|
256
|
+
const result = compile(source)
|
|
257
|
+
const errs = result.errors.filter(
|
|
258
|
+
(e) => e.code === ErrorCodes.MISSING_KEY_IN_LIST || e.code === ErrorCodes.MISSING_KEY_IN_NESTED_LIST,
|
|
259
|
+
)
|
|
260
|
+
expect(errs).toHaveLength(0)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('|| body without key raises BF023', () => {
|
|
264
|
+
const source = `
|
|
265
|
+
interface Item { hidden: boolean; id: string }
|
|
266
|
+
export function List({ items }: { items: Item[] }) {
|
|
267
|
+
return <ul>{items.map(item => item.hidden || <li>{item.id}</li>)}</ul>
|
|
268
|
+
}
|
|
269
|
+
`
|
|
270
|
+
const errs = errorsFor(ErrorCodes.MISSING_KEY_IN_LIST, source)
|
|
271
|
+
expect(errs).toHaveLength(1)
|
|
272
|
+
expect(errs[0].severity).toBe('error')
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
227
276
|
// ---------------------------------------------------------------------------
|
|
228
277
|
// BF024 — nested map
|
|
229
278
|
// ---------------------------------------------------------------------------
|
|
@@ -236,6 +236,47 @@ describe('reactive attributes inside .map() callbacks', () => {
|
|
|
236
236
|
expect(forEachMatches.length).toBe(1)
|
|
237
237
|
})
|
|
238
238
|
|
|
239
|
+
test('per-item attr reading an outer signal via helper+index gets a createEffect (#1673)', () => {
|
|
240
|
+
// A per-item attribute whose value reads the source array signal through
|
|
241
|
+
// a helper indexed by position (`widthAt(i)` → `items()[i].w`) used to
|
|
242
|
+
// emit NO reactive effect — the value was baked into the item template
|
|
243
|
+
// once and froze at its SSR value. The identical binding on a top-level
|
|
244
|
+
// element works, because the top-level path wraps opaque function calls
|
|
245
|
+
// via the Solid-style AST-flag fallback (#940). This test pins that the
|
|
246
|
+
// loop-child path now applies the same fallback.
|
|
247
|
+
const source = `
|
|
248
|
+
'use client'
|
|
249
|
+
import { createSignal } from '@barefootjs/client'
|
|
250
|
+
|
|
251
|
+
export function ReproLoopAttr() {
|
|
252
|
+
const [items, setItems] = createSignal([{ id: 'a', w: 20 }, { id: 'b', w: 50 }])
|
|
253
|
+
const widthAt = (i) => items()[i].w
|
|
254
|
+
return (
|
|
255
|
+
<ul>
|
|
256
|
+
{items().map((it, i) => (
|
|
257
|
+
<li key={it.id} data-b style={\`width:\${widthAt(i)}%\`}>{it.id}</li>
|
|
258
|
+
))}
|
|
259
|
+
</ul>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
`
|
|
263
|
+
const result = compileJSX(source, 'ReproLoopAttr.tsx', { adapter })
|
|
264
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
265
|
+
|
|
266
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
267
|
+
// The per-item style binding must be wired through a createEffect that
|
|
268
|
+
// re-reads the helper and writes the style attribute, so it tracks the
|
|
269
|
+
// `items()` signal and updates after hydration. Pin the helper call
|
|
270
|
+
// *inside* the createEffect alongside the style write — `widthAt(` also
|
|
271
|
+
// appears in the static template clone, so asserting it independently
|
|
272
|
+
// could pass even if the effect was missing (the exact regression here).
|
|
273
|
+
// The index resolves to the loop's renderItem index param, in scope
|
|
274
|
+
// inside the factory.
|
|
275
|
+
expect(clientJs).toMatch(
|
|
276
|
+
/createEffect\(\(\)\s*=>\s*\{[\s\S]*?widthAt\([\s\S]*?setAttribute\('style'/,
|
|
277
|
+
)
|
|
278
|
+
})
|
|
279
|
+
|
|
239
280
|
test('keyed loop: `key` prop is not emitted as a reactive DOM attribute', () => {
|
|
240
281
|
// Regression guard for the "key duplicate emission" bug:
|
|
241
282
|
// `<li key={item.id}>` used to be both rendered as `data-key=` in the
|
|
@@ -129,6 +129,33 @@ describe('tokenContainsIdent', () => {
|
|
|
129
129
|
})
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
+
// Regex literals were invisible to the previous hand-rolled char scanner:
|
|
133
|
+
// a lone quote inside the regex flipped it into string state (swallowing
|
|
134
|
+
// real references), and an identifier inside the regex body was counted as
|
|
135
|
+
// a reference. The shared ts.createScanner-based lexer recognises regex
|
|
136
|
+
// literals, so both cases are now correct (#1370).
|
|
137
|
+
describe('regex literals', () => {
|
|
138
|
+
test('reference after a regex literal containing an apostrophe', () => {
|
|
139
|
+
expect(tokenContainsIdent("/it's/.test(className)", 'className')).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('reference after a regex literal containing a quote', () => {
|
|
143
|
+
expect(tokenContainsIdent('/a"b/.test(className)', 'className')).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('identifier inside a regex body is not a reference', () => {
|
|
147
|
+
expect(tokenContainsIdent('/className/.test(x)', 'className')).toBe(false)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('regex with escaped slash does not leak into following code', () => {
|
|
151
|
+
expect(tokenContainsIdent('/a\\/b/.test(className)', 'className')).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('division is not mistaken for a regex literal', () => {
|
|
155
|
+
expect(tokenContainsIdent('total / className', 'className')).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
132
159
|
describe('non-matches', () => {
|
|
133
160
|
test('substring is not a match (word boundary)', () => {
|
|
134
161
|
expect(tokenContainsIdent('myClassName', 'className')).toBe(false)
|
|
@@ -136,21 +136,22 @@ describe('Unsupported Expression Error (BF021)', () => {
|
|
|
136
136
|
})
|
|
137
137
|
|
|
138
138
|
describe('Unsupported Sort Comparator (BF021)', () => {
|
|
139
|
-
test('emits BF021 for
|
|
140
|
-
// #1448 Tier B widened the
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
139
|
+
test('emits BF021 for function-reference comparator — outside accepted catalogue', () => {
|
|
140
|
+
// #1448 Tier B follow-up widened the catalogue to include
|
|
141
|
+
// multi-key (`a.x - b.x || a.y - b.y`), relational ternary, and
|
|
142
|
+
// single-`return` block bodies. Function-reference comparators
|
|
143
|
+
// (`arr.sort(cmp)` where `cmp` is a named function) are still out
|
|
144
|
+
// of scope — they need scope resolution and refuse here.
|
|
145
145
|
const source = `
|
|
146
146
|
'use client'
|
|
147
147
|
import { createSignal } from '@barefootjs/client'
|
|
148
148
|
|
|
149
149
|
export function TodoList() {
|
|
150
150
|
const [items, setItems] = createSignal<any[]>([])
|
|
151
|
+
const cmp = (a, b) => a.priority - b.priority
|
|
151
152
|
return (
|
|
152
153
|
<ul>
|
|
153
|
-
{items().sort(
|
|
154
|
+
{items().sort(cmp).map(t => (
|
|
154
155
|
<li>{t.name}</li>
|
|
155
156
|
))}
|
|
156
157
|
</ul>
|
|
@@ -162,7 +163,6 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
162
163
|
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
163
164
|
|
|
164
165
|
expect(bf021).toHaveLength(1)
|
|
165
|
-
expect(bf021[0].message).toContain('not a supported shape')
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
test('@client suppresses BF021 for unsupported sort comparator', () => {
|
|
@@ -172,9 +172,10 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
172
172
|
|
|
173
173
|
export function TodoList() {
|
|
174
174
|
const [items, setItems] = createSignal<any[]>([])
|
|
175
|
+
const cmp = (a, b) => a.priority - b.priority
|
|
175
176
|
return (
|
|
176
177
|
<ul>
|
|
177
|
-
{/* @client */ items().sort(
|
|
178
|
+
{/* @client */ items().sort(cmp).map(t => (
|
|
178
179
|
<li>{t.name}</li>
|
|
179
180
|
))}
|
|
180
181
|
</ul>
|
|
@@ -188,9 +189,37 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
188
189
|
expect(bf021).toHaveLength(0)
|
|
189
190
|
})
|
|
190
191
|
|
|
191
|
-
test('emits BF021
|
|
192
|
-
//
|
|
193
|
-
//
|
|
192
|
+
test('emits BF021 for localeCompare with a locale/options argument', () => {
|
|
193
|
+
// The zero-arg `a.f.localeCompare(b.f)` form lowers, but the
|
|
194
|
+
// locale/options form needs per-adapter collation plumbing and
|
|
195
|
+
// stays refused (deferred #1448 Tier B follow-up).
|
|
196
|
+
const source = `
|
|
197
|
+
'use client'
|
|
198
|
+
import { createSignal } from '@barefootjs/client'
|
|
199
|
+
|
|
200
|
+
export function TodoList() {
|
|
201
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
202
|
+
return (
|
|
203
|
+
<ul>
|
|
204
|
+
{items().sort((a, b) => a.name.localeCompare(b.name, 'en', { numeric: true })).map(t => (
|
|
205
|
+
<li>{t.name}</li>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
`
|
|
211
|
+
|
|
212
|
+
const { errors } = compileToIR(source)
|
|
213
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
214
|
+
|
|
215
|
+
expect(bf021).toHaveLength(1)
|
|
216
|
+
expect(bf021[0].message).toContain('not a supported shape')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('emits BF021 error for multi-statement block-body sort comparator', () => {
|
|
220
|
+
// Single-`return` block bodies now lower (#1448 Tier B follow-up),
|
|
221
|
+
// but multi-statement / local-var bodies stay refused — generalising
|
|
222
|
+
// over arbitrary statement sequences isn't tractable in a template.
|
|
194
223
|
const source = `
|
|
195
224
|
'use client'
|
|
196
225
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -199,7 +228,7 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
199
228
|
const [items, setItems] = createSignal<any[]>([])
|
|
200
229
|
return (
|
|
201
230
|
<ul>
|
|
202
|
-
{items().sort((a, b) => {
|
|
231
|
+
{items().sort((a, b) => { const x = a.price; return x - b.price }).map(t => (
|
|
203
232
|
<li>{t.name}</li>
|
|
204
233
|
))}
|
|
205
234
|
</ul>
|
|
@@ -123,6 +123,26 @@ export interface TemplateAdapter {
|
|
|
123
123
|
* Required for adapters that look up templates by filename (e.g. Mojolicious).
|
|
124
124
|
*/
|
|
125
125
|
templatesPerComponent?: boolean
|
|
126
|
+
/**
|
|
127
|
+
* How the application author injects the externals importmap (and any
|
|
128
|
+
* `<link rel="modulepreload">` hints) into the page `<head>` when
|
|
129
|
+
* `externals` / `bundleEntries` are configured.
|
|
130
|
+
*
|
|
131
|
+
* - `'component'` — the adapter ships a render-time component (e.g. Hono's
|
|
132
|
+
* `BfImportMap`) that reads `barefoot-externals.json`; `bf build` emits no
|
|
133
|
+
* static snippet.
|
|
134
|
+
* - `'html-snippet'` — the adapter targets a template-string language (Go
|
|
135
|
+
* html/template, Mojolicious EP) with no component layer, so `bf build`
|
|
136
|
+
* writes a ready-to-include `barefoot-importmap.html` alongside
|
|
137
|
+
* `barefoot-externals.json` (via `renderImportMapHtml`).
|
|
138
|
+
*
|
|
139
|
+
* Optional only for backward compatibility (and internal-only adapters like
|
|
140
|
+
* the CSR test adapter). Every *shipping* adapter must set it — the
|
|
141
|
+
* adapter-tests importmap-injection contract enforces this so a new adapter
|
|
142
|
+
* cannot silently leave configured `externals` with no injection point.
|
|
143
|
+
* See issue #1644.
|
|
144
|
+
*/
|
|
145
|
+
importMapInjection?: 'component' | 'html-snippet'
|
|
126
146
|
/**
|
|
127
147
|
* Module specifier of the SSR shim for `@barefootjs/client` (and
|
|
128
148
|
* `/runtime`). When set, the compiler rewrites client-package imports in
|
|
@@ -138,11 +138,26 @@ export class TestAdapter extends JsxAdapter {
|
|
|
138
138
|
}
|
|
139
139
|
const fullPropsDestructure = `{ ${parts.join(', ')} }`
|
|
140
140
|
|
|
141
|
+
// Default the props param to `{}` when the component has no required
|
|
142
|
+
// props, so a bare no-arg call (`Foo()`) doesn't crash on destructuring
|
|
143
|
+
// `undefined`. This is what makes a JSX-returning arrow hoisted from an
|
|
144
|
+
// object-literal value (e.g. `THEME_LOGOS[id]()`) renderable at SSR
|
|
145
|
+
// (#1663). `hasRequiredProps` ignores props that carry a destructuring
|
|
146
|
+
// default, but the declared props type may still mark that field
|
|
147
|
+
// required — so a bare `= {}` would fail `tsc` ("Property 'x' is missing
|
|
148
|
+
// in type '{}'..."). Assert the default to the param's own annotated type
|
|
149
|
+
// (`{} as T`); the destructuring defaults supply the values at runtime.
|
|
150
|
+
const hasRequiredProps = ir.metadata.propsParams.some(
|
|
151
|
+
(p: ParamInfo) => !p.optional && p.defaultValue === undefined && !p.isRest,
|
|
152
|
+
)
|
|
153
|
+
const propsTypeExpr = typeAnnotation.replace(/^:\s*/, '')
|
|
154
|
+
const noArgDefault = hasRequiredProps ? '' : ` = {} as ${propsTypeExpr}`
|
|
155
|
+
|
|
141
156
|
const lines: string[] = []
|
|
142
157
|
// Module-export keyword belongs to the adapter: it knows the target language
|
|
143
158
|
// and whether the source declared the component as exported.
|
|
144
159
|
const exportPrefix = ir.metadata.isExported === false ? '' : 'export '
|
|
145
|
-
lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}) {`)
|
|
160
|
+
lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`)
|
|
146
161
|
|
|
147
162
|
// Generate scope ID
|
|
148
163
|
if (hasClientInteractivity) {
|