@barefootjs/jsx 0.16.0 → 0.17.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.
Files changed (65) hide show
  1. package/dist/adapters/env-signal.d.ts +73 -15
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  4. package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
  5. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  6. package/dist/analyzer-context.d.ts +29 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/builtin-lowering-plugins.d.ts +34 -0
  10. package/dist/builtin-lowering-plugins.d.ts.map +1 -0
  11. package/dist/compiler.d.ts.map +1 -1
  12. package/dist/expression-parser.d.ts +264 -163
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +10 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7839 -7019
  17. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
  20. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  21. package/dist/jsx-to-ir.d.ts.map +1 -1
  22. package/dist/lowering-registry.d.ts +122 -0
  23. package/dist/lowering-registry.d.ts.map +1 -0
  24. package/dist/query-href-lowering.d.ts +63 -0
  25. package/dist/query-href-lowering.d.ts.map +1 -0
  26. package/dist/ssr-defaults.d.ts.map +1 -1
  27. package/dist/ssr-seed-plan.d.ts +84 -0
  28. package/dist/ssr-seed-plan.d.ts.map +1 -0
  29. package/dist/types.d.ts +180 -11
  30. package/dist/types.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
  33. package/src/__tests__/analyzer.test.ts +53 -0
  34. package/src/__tests__/expression-parser.test.ts +714 -392
  35. package/src/__tests__/free-identifiers.test.ts +55 -0
  36. package/src/__tests__/ir-reduce-op.test.ts +18 -21
  37. package/src/__tests__/ir-sort-comparator.test.ts +19 -20
  38. package/src/__tests__/lowering-registry.test.ts +141 -0
  39. package/src/__tests__/materialize-getter-calls.test.ts +58 -0
  40. package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
  41. package/src/__tests__/query-href-recognition.test.ts +58 -0
  42. package/src/__tests__/serialize-parsed-expr.test.ts +223 -0
  43. package/src/__tests__/ssr-seed-plan.test.ts +212 -0
  44. package/src/__tests__/unsupported-expression.test.ts +98 -4
  45. package/src/adapters/env-signal.ts +108 -21
  46. package/src/adapters/jsx-adapter.ts +17 -0
  47. package/src/adapters/parsed-expr-emitter.ts +39 -41
  48. package/src/analyzer-context.ts +72 -27
  49. package/src/analyzer.ts +226 -9
  50. package/src/builtin-lowering-plugins.ts +54 -0
  51. package/src/compiler.ts +6 -1
  52. package/src/expression-parser.ts +1375 -929
  53. package/src/index.ts +31 -3
  54. package/src/ir-to-client-js/csr-substitute.ts +5 -0
  55. package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
  56. package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
  57. package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
  58. package/src/jsx-to-ir.ts +182 -43
  59. package/src/lowering-registry.ts +160 -0
  60. package/src/query-href-lowering.ts +147 -0
  61. package/src/ssr-defaults.ts +5 -1
  62. package/src/ssr-seed-plan.ts +146 -0
  63. package/src/types.ts +182 -12
  64. package/src/__tests__/flatmap-support.test.ts +0 -218
  65. package/src/__tests__/reduce-op.test.ts +0 -201
@@ -1,218 +0,0 @@
1
- /**
2
- * BarefootJS Compiler - .flatMap() support (#1554 / #1448 Tier C)
3
- *
4
- * flatMap callbacks containing JSX must compile to valid JS for
5
- * JavaScript runtime adapters (Hono, CSR). Template-language adapters
6
- * (Go, Mojo) do not support flatMap — the workaround is .map() + Fragment.
7
- */
8
-
9
- import { describe, test, expect } from 'bun:test'
10
- import { compileJSX } from '../compiler'
11
- import { TestAdapter } from '../adapters/test-adapter'
12
- import { HonoAdapter } from '../../../../packages/adapter-hono/src/adapter/hono-adapter'
13
-
14
- const adapter = new TestAdapter()
15
-
16
- describe('.flatMap() — simple arrow with array literal', () => {
17
- test('arrow body returning [<A/>, <B/>] compiles to flatMap with valid JS', () => {
18
- const source = `
19
- 'use client'
20
-
21
- export function DL(props: { items: { term: string; def: string }[] }) {
22
- return (
23
- <dl>
24
- {props.items.flatMap((item, i) => [
25
- <dt key={\`dt-\${i}\`}>{item.term}</dt>,
26
- <dd key={\`dd-\${i}\`}>{item.def}</dd>
27
- ])}
28
- </dl>
29
- )
30
- }
31
- `
32
- const result = compileJSX(source, 'DL.tsx', { adapter })
33
- expect(result.errors).toHaveLength(0)
34
-
35
- const clientJs = result.files.find(f => f.type === 'clientJs')!
36
- expect(clientJs.content).toContain('.flatMap(')
37
- expect(clientJs.content).toContain('.join(')
38
- // Template contains compiled HTML (not raw JSX)
39
- expect(clientJs.content).toContain('item.term')
40
- expect(clientJs.content).toContain('item.def')
41
- })
42
- })
43
-
44
- describe('.flatMap() — complex block body with conditional returns', () => {
45
- test('block body with variable JSX and conditional returns produces valid client JS', () => {
46
- const source = `
47
- 'use client'
48
-
49
- export function Timeline(props: { frames: { label: string }[] }) {
50
- return (
51
- <div>
52
- {props.frames.flatMap((frame, i) => {
53
- const panel = (
54
- <ResizablePanel key={\`p-\${i}\`}>
55
- {i + 1}
56
- </ResizablePanel>
57
- )
58
- if (i === 0) return [panel]
59
- return [<ResizableHandle key={\`h-\${i}\`} />, panel]
60
- })}
61
- </div>
62
- )
63
- }
64
- `
65
- const result = compileJSX(source, 'Timeline.tsx', { adapter })
66
- expect(result.errors).toHaveLength(0)
67
-
68
- const clientJs = result.files.find(f => f.type === 'clientJs')!
69
- expect(clientJs.content).toContain('.flatMap(')
70
- expect(clientJs.content).toContain('.join(')
71
- // JSX should be compiled to renderChild calls, not raw JSX
72
- expect(clientJs.content).toContain("renderChild('ResizablePanel'")
73
- expect(clientJs.content).toContain("renderChild('ResizableHandle'")
74
- expect(clientJs.content).not.toContain('<ResizablePanel')
75
- expect(clientJs.content).not.toContain('<ResizableHandle')
76
- })
77
- })
78
-
79
- describe('.flatMap() — Hono adapter', () => {
80
- test('Hono preserves JSX in flatMap callback', () => {
81
- const source = `
82
- 'use client'
83
-
84
- export function Timeline(props: { frames: { label: string }[] }) {
85
- return (
86
- <div>
87
- {props.frames.flatMap((frame, i) => {
88
- const panel = (
89
- <ResizablePanel key={\`p-\${i}\`}>
90
- {i + 1}
91
- </ResizablePanel>
92
- )
93
- if (i === 0) return [panel]
94
- return [<ResizableHandle key={\`h-\${i}\`} />, panel]
95
- })}
96
- </div>
97
- )
98
- }
99
- `
100
- const result = compileJSX(source, 'Timeline.tsx', { adapter: new HonoAdapter() })
101
- expect(result.errors).toHaveLength(0)
102
-
103
- const markedTemplate = result.files.find(f => f.type === 'markedTemplate')!
104
- // Hono should use flatMap (not map)
105
- expect(markedTemplate.content).toContain('.flatMap(')
106
- // Hono preserves JSX natively
107
- expect(markedTemplate.content).toContain('<ResizablePanel')
108
- expect(markedTemplate.content).toContain('<ResizableHandle')
109
- })
110
-
111
- test('simple flatMap arrow with array literal works in Hono', () => {
112
- const source = `
113
- 'use client'
114
-
115
- export function DL(props: { items: { term: string; def: string }[] }) {
116
- return (
117
- <dl>
118
- {props.items.flatMap((item, i) => [
119
- <dt key={\`dt-\${i}\`}>{item.term}</dt>,
120
- <dd key={\`dd-\${i}\`}>{item.def}</dd>
121
- ])}
122
- </dl>
123
- )
124
- }
125
- `
126
- const result = compileJSX(source, 'DL.tsx', { adapter: new HonoAdapter() })
127
- expect(result.errors).toHaveLength(0)
128
-
129
- const markedTemplate = result.files.find(f => f.type === 'markedTemplate')!
130
- expect(markedTemplate.content).toContain('.flatMap(')
131
- })
132
- })
133
-
134
- describe('.flatMap() — single JSX return (same as map)', () => {
135
- test('flatMap with single JSX return compiles like map', () => {
136
- const source = `
137
- 'use client'
138
-
139
- export function List(props: { items: string[] }) {
140
- return (
141
- <ul>
142
- {props.items.flatMap((item, i) => (
143
- <li key={i}>{item}</li>
144
- ))}
145
- </ul>
146
- )
147
- }
148
- `
149
- const result = compileJSX(source, 'List.tsx', { adapter })
150
- expect(result.errors).toHaveLength(0)
151
-
152
- const clientJs = result.files.find(f => f.type === 'clientJs')!
153
- expect(clientJs.content).toContain('.flatMap(')
154
- // Template contains compiled HTML template literal, not raw JSX
155
- expect(clientJs.content).toContain('data-key')
156
- })
157
- })
158
-
159
- describe('.flatMap() — variable-assigned result (#1554 comment)', () => {
160
- test('flatMap stored in const, then used in JSX, compiles without raw JSX', () => {
161
- const source = `
162
- 'use client'
163
-
164
- export function TimelineBar(props: { items: string[] }) {
165
- const children = props.items.flatMap((item, i) => {
166
- const panel = (
167
- <ResizablePanel key={item} defaultSize={50} className="segment">
168
- <span>{i + 1}</span>
169
- </ResizablePanel>
170
- )
171
- if (i === 0) return [panel]
172
- return [<ResizableHandle key={\`h-\${item}\`} />, panel]
173
- })
174
-
175
- return (
176
- <ResizablePanelGroup direction="horizontal">
177
- {children}
178
- </ResizablePanelGroup>
179
- )
180
- }
181
- `
182
- const result = compileJSX(source, 'TimelineBar.tsx', { adapter })
183
- expect(result.errors).toHaveLength(0)
184
-
185
- const clientJs = result.files.find(f => f.type === 'clientJs')!
186
- // flatMap should be used (not map)
187
- expect(clientJs.content).toContain('.flatMap(')
188
- // JSX should be compiled to renderChild calls, not raw JSX
189
- expect(clientJs.content).toContain("renderChild('ResizablePanel'")
190
- expect(clientJs.content).toContain("renderChild('ResizableHandle'")
191
- expect(clientJs.content).not.toContain('<ResizablePanel')
192
- expect(clientJs.content).not.toContain('<ResizableHandle')
193
- // The raw const declaration should not appear in the init function
194
- expect(clientJs.content).not.toContain('const children = ')
195
- })
196
-
197
- test('map() stored in const with JSX also gets inlined', () => {
198
- const source = `
199
- 'use client'
200
-
201
- export function List(props: { items: string[] }) {
202
- const rendered = props.items.map((item, i) => (
203
- <ListItem key={i} label={item} />
204
- ))
205
-
206
- return (
207
- <ul>{rendered}</ul>
208
- )
209
- }
210
- `
211
- const result = compileJSX(source, 'List.tsx', { adapter })
212
- expect(result.errors).toHaveLength(0)
213
-
214
- const clientJs = result.files.find(f => f.type === 'clientJs')!
215
- expect(clientJs.content).not.toContain('<ListItem')
216
- expect(clientJs.content).not.toContain('const rendered = ')
217
- })
218
- })
@@ -1,201 +0,0 @@
1
- import { describe, test, expect } from 'bun:test'
2
- import { parseExpression, isSupported, stringifyParsedExpr } from '../expression-parser'
3
-
4
- /**
5
- * `.reduce(fn, init)` arithmetic-fold catalogue (#1448 Tier C). The
6
- * parser intercepts the accepted shapes into a structured `ReduceOp`
7
- * (mirroring the `.sort` `SortComparator` precedent) and refuses
8
- * everything else to `unsupported` so the template adapters surface
9
- * BF101 with the `@client` escape hatch.
10
- */
11
- describe('reduce() arithmetic-fold catalogue', () => {
12
- test('numeric sum over a field → field/numeric', () => {
13
- const r = parseExpression('items.reduce((sum, t) => sum + t.duration, 0)')
14
- expect(r.kind).toBe('array-method')
15
- if (r.kind === 'array-method' && r.method === 'reduce') {
16
- expect(r.reduceOp.op).toBe('+')
17
- expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'duration' })
18
- expect(r.reduceOp.type).toBe('numeric')
19
- expect(r.reduceOp.init).toBe('0')
20
- expect(r.reduceOp.paramAcc).toBe('sum')
21
- expect(r.reduceOp.paramItem).toBe('t')
22
- }
23
- expect(isSupported(r).supported).toBe(true)
24
- })
25
-
26
- test('numeric sum over self (primitive array) → self/numeric', () => {
27
- const r = parseExpression('nums.reduce((a, b) => a + b, 0)')
28
- expect(r.kind).toBe('array-method')
29
- if (r.kind === 'array-method' && r.method === 'reduce') {
30
- expect(r.reduceOp.op).toBe('+')
31
- expect(r.reduceOp.key).toEqual({ kind: 'self' })
32
- expect(r.reduceOp.type).toBe('numeric')
33
- }
34
- })
35
-
36
- test('reduceRight shares the catalogue and preserves the method name', () => {
37
- const r = parseExpression("items.reduceRight((acc, x) => acc + x.label, '')")
38
- expect(r.kind).toBe('array-method')
39
- if (r.kind === 'array-method') {
40
- expect(r.method).toBe('reduceRight')
41
- if (r.method === 'reduceRight') {
42
- expect(r.reduceOp.op).toBe('+')
43
- expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'label' })
44
- expect(r.reduceOp.type).toBe('string')
45
- }
46
- }
47
- })
48
-
49
- test('reduceRight round-trips with its method name preserved', () => {
50
- const r = parseExpression('nums.reduceRight((a, b) => a + b, 0)')
51
- expect(stringifyParsedExpr(r)).toBe('nums.reduceRight((a,b) => a + b, 0)')
52
- })
53
-
54
- test('product over a field with init 1 → field/numeric/*', () => {
55
- const r = parseExpression('items.reduce((acc, x) => acc * x.qty, 1)')
56
- expect(r.kind).toBe('array-method')
57
- if (r.kind === 'array-method' && r.method === 'reduce') {
58
- expect(r.reduceOp.op).toBe('*')
59
- expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'qty' })
60
- expect(r.reduceOp.type).toBe('numeric')
61
- expect(r.reduceOp.init).toBe('1')
62
- }
63
- })
64
-
65
- test('string init makes + a concatenation fold (init is the decoded value)', () => {
66
- const r = parseExpression("items.reduce((acc, x) => acc + x.label, '')")
67
- expect(r.kind).toBe('array-method')
68
- if (r.kind === 'array-method' && r.method === 'reduce') {
69
- expect(r.reduceOp.op).toBe('+')
70
- expect(r.reduceOp.type).toBe('string')
71
- expect(r.reduceOp.init).toBe('') // decoded contents, not the `''` source
72
- }
73
- })
74
-
75
- test('non-empty string seed decodes to its contents', () => {
76
- const r = parseExpression("items.reduce((acc, x) => acc + x.label, ', ')")
77
- expect(r.kind).toBe('array-method')
78
- if (r.kind === 'array-method' && r.method === 'reduce') {
79
- expect(r.reduceOp.init).toBe(', ')
80
- }
81
- })
82
-
83
- test('numeric init normalises separators / radix to canonical decimal', () => {
84
- // `.text` gives TS's canonical decimal so Go ParseFloat + Perl agree
85
- // (#1728 review: raw `1_000` / `0x10` would mis-parse on Go).
86
- for (const [src, expected] of [
87
- ['1_000', '1000'],
88
- ['0x10', '16'],
89
- ['1e3', '1000'],
90
- ] as const) {
91
- const r = parseExpression(`nums.reduce((a, b) => a + b, ${src})`)
92
- expect(r.kind).toBe('array-method')
93
- if (r.kind === 'array-method' && r.method === 'reduce') {
94
- expect(r.reduceOp.init).toBe(expected)
95
- }
96
- }
97
- })
98
-
99
- test('parenthesized numeric init is unwrapped and accepted', () => {
100
- for (const [src, expected] of [
101
- ['(0)', '0'],
102
- ['(-1)', '-1'],
103
- ] as const) {
104
- const r = parseExpression(`nums.reduce((a, b) => a + b, ${src})`)
105
- expect(r.kind).toBe('array-method')
106
- if (r.kind === 'array-method' && r.method === 'reduce') {
107
- expect(r.reduceOp.init).toBe(expected)
108
- }
109
- }
110
- })
111
-
112
- test('an escape-free seed containing an apostrophe is accepted (decoded contents kept)', () => {
113
- // `"a'b"` is escape-free (decoded === raw inner), so it's accepted;
114
- // the decoded value carries the apostrophe, which the Mojo emit
115
- // single-quote-escapes.
116
- const r = parseExpression(`items.reduce((acc, x) => acc + x.l, "a'b")`)
117
- expect(r.kind).toBe('array-method')
118
- if (r.kind === 'array-method' && r.method === 'reduce') {
119
- expect(r.reduceOp.init).toBe("a'b")
120
- }
121
- })
122
-
123
- test('single-return block body unwraps to the fold expression', () => {
124
- const r = parseExpression('items.reduce((sum, t) => { return sum + t.n }, 0)')
125
- expect(r.kind).toBe('array-method')
126
- if (r.kind === 'array-method' && r.method === 'reduce') {
127
- expect(r.reduceOp.key).toEqual({ kind: 'field', field: 'n' })
128
- }
129
- })
130
-
131
- test('negative numeric init is accepted', () => {
132
- const r = parseExpression('nums.reduce((a, b) => a + b, -1)')
133
- expect(r.kind).toBe('array-method')
134
- if (r.kind === 'array-method' && r.method === 'reduce') {
135
- expect(r.reduceOp.init).toBe('-1')
136
- }
137
- })
138
-
139
- test('round-trips a numeric fold back to valid JS via stringifyParsedExpr', () => {
140
- const r = parseExpression('items.reduce((sum, t) => sum + t.duration, 0)')
141
- expect(stringifyParsedExpr(r)).toBe('items.reduce((sum,t) => sum + t.duration, 0)')
142
- })
143
-
144
- test('round-trips a string fold by re-quoting the decoded seed', () => {
145
- const r = parseExpression("items.reduce((a, x) => a + x.l, ', ')")
146
- // Decoded seed `, ` re-quoted via JSON.stringify → a valid JS string.
147
- expect(stringifyParsedExpr(r)).toBe('items.reduce((a,x) => a + x.l, ", ")')
148
- })
149
-
150
- describe('refused shapes → unsupported (BF101)', () => {
151
- test('missing initial value', () => {
152
- // A no-init reduce isn't intercepted (it needs 2 args); it falls
153
- // through to a generic `call` that the UNSUPPORTED_METHODS gate
154
- // refuses — JS throws on an empty array here, so a template can't
155
- // mirror it cleanly.
156
- const r = parseExpression('items.reduce((sum, t) => sum + t.duration)')
157
- expect(isSupported(r).supported).toBe(false)
158
- })
159
-
160
- test('string concat with * is rejected', () => {
161
- const r = parseExpression("items.reduce((acc, x) => acc * x.label, '')")
162
- expect(r.kind).toBe('unsupported')
163
- })
164
-
165
- test('accumulator on the right operand is rejected', () => {
166
- const r = parseExpression('items.reduce((sum, t) => t.duration + sum, 0)')
167
- expect(r.kind).toBe('unsupported')
168
- })
169
-
170
- test('non-literal init is rejected', () => {
171
- const r = parseExpression('items.reduce((sum, t) => sum + t.n, start)')
172
- expect(r.kind).toBe('unsupported')
173
- })
174
-
175
- test('object-building reducer is rejected', () => {
176
- const r = parseExpression('items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {})')
177
- expect(r.kind).toBe('unsupported')
178
- })
179
-
180
- test('deep field access is rejected', () => {
181
- const r = parseExpression('items.reduce((sum, t) => sum + t.a.b, 0)')
182
- expect(r.kind).toBe('unsupported')
183
- })
184
-
185
- test('string seed carrying an escape is rejected (cross-adapter safety)', () => {
186
- // A seed with an escape can't be guaranteed byte-equal across the
187
- // Go-template / Perl string embeddings without per-target decoding,
188
- // so it refuses to `unsupported` rather than risk divergence.
189
- const r = parseExpression("items.reduce((acc, x) => acc + x.l, '\\n')")
190
- expect(r.kind).toBe('unsupported')
191
- })
192
-
193
- test('reduceRight without an init is refused (like reduce)', () => {
194
- // The matching 2-arg form is intercepted; a no-init reduceRight
195
- // falls through to a generic call the UNSUPPORTED_METHODS gate
196
- // refuses (JS throws on an empty array there).
197
- const rr = parseExpression('items.reduceRight((sum, t) => sum + t.n)')
198
- expect(isSupported(rr).supported).toBe(false)
199
- })
200
- })
201
- })