@barefootjs/jsx 0.15.2 → 0.17.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 +38 -15
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +29 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts +34 -0
- package/dist/builtin-lowering-plugins.d.ts.map +1 -0
- package/dist/expression-parser.d.ts +219 -163
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6892 -6118
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +122 -0
- package/dist/lowering-registry.d.ts.map +1 -0
- package/dist/profiler.d.ts +115 -0
- package/dist/profiler.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts +63 -0
- package/dist/query-href-lowering.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +169 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
- package/src/__tests__/analyzer.test.ts +53 -0
- package/src/__tests__/expression-parser.test.ts +703 -391
- package/src/__tests__/ir-reduce-op.test.ts +18 -21
- package/src/__tests__/ir-sort-comparator.test.ts +19 -20
- package/src/__tests__/lowering-registry.test.ts +141 -0
- package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
- package/src/__tests__/profiler.test.ts +149 -0
- package/src/__tests__/query-href-recognition.test.ts +58 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +204 -0
- package/src/__tests__/unsupported-expression.test.ts +98 -4
- package/src/adapters/env-signal.ts +60 -21
- package/src/adapters/jsx-adapter.ts +17 -0
- package/src/adapters/parsed-expr-emitter.ts +39 -41
- package/src/analyzer-context.ts +72 -27
- package/src/analyzer.ts +226 -9
- package/src/builtin-lowering-plugins.ts +54 -0
- package/src/expression-parser.ts +1183 -927
- package/src/index.ts +35 -3
- package/src/ir-to-client-js/csr-substitute.ts +5 -0
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
- package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
- package/src/jsx-to-ir.ts +182 -43
- package/src/lowering-registry.ts +160 -0
- package/src/profiler.ts +328 -0
- package/src/query-href-lowering.ts +147 -0
- package/src/ssr-defaults.ts +5 -1
- package/src/types.ts +171 -12
- package/src/__tests__/flatmap-support.test.ts +0 -218
- package/src/__tests__/reduce-op.test.ts +0 -201
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { parseExpression, serializeParsedExpr, freeVarsInBody } from '../expression-parser'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `serializeParsedExpr` emits the minimal JSON the runtime evaluator (Go
|
|
6
|
+
* `eval.go` `EvalNode` / Perl `Evaluator.pm` `evaluate`) reads. These tests pin
|
|
7
|
+
* the field contract (only evaluator-read fields per kind) and the purity gate
|
|
8
|
+
* (folded / non-expression kinds → null). `freeVarsInBody` pins the captured
|
|
9
|
+
* free-var set used to build the evaluator's `base_env`. (#2018)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Parse a callback-body source and serialize it to the evaluator JSON object. */
|
|
13
|
+
function evalJSON(src: string): unknown {
|
|
14
|
+
const s = serializeParsedExpr(parseExpression(src))
|
|
15
|
+
return s === null ? null : JSON.parse(s)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('serializeParsedExpr', () => {
|
|
19
|
+
test('literal carries only `value` (no literalType / raw)', () => {
|
|
20
|
+
expect(evalJSON('42')).toEqual({ kind: 'literal', value: 42 })
|
|
21
|
+
expect(evalJSON("'hi'")).toEqual({ kind: 'literal', value: 'hi' })
|
|
22
|
+
expect(evalJSON('true')).toEqual({ kind: 'literal', value: true })
|
|
23
|
+
expect(evalJSON('null')).toEqual({ kind: 'literal', value: null })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('identifier carries `name`', () => {
|
|
27
|
+
expect(evalJSON('acc')).toEqual({ kind: 'identifier', name: 'acc' })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('reducer body: binary over member projection', () => {
|
|
31
|
+
expect(evalJSON('acc + item.price')).toEqual({
|
|
32
|
+
kind: 'binary',
|
|
33
|
+
op: '+',
|
|
34
|
+
left: { kind: 'identifier', name: 'acc' },
|
|
35
|
+
// member carries object + property only (no `computed`).
|
|
36
|
+
right: {
|
|
37
|
+
kind: 'member',
|
|
38
|
+
object: { kind: 'identifier', name: 'item' },
|
|
39
|
+
property: 'price',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('comparator body: 3-way ternary', () => {
|
|
45
|
+
expect(evalJSON('a > b ? 1 : a < b ? -1 : 0')).toEqual({
|
|
46
|
+
kind: 'conditional',
|
|
47
|
+
test: { kind: 'binary', op: '>', left: { kind: 'identifier', name: 'a' }, right: { kind: 'identifier', name: 'b' } },
|
|
48
|
+
consequent: { kind: 'literal', value: 1 },
|
|
49
|
+
alternate: {
|
|
50
|
+
kind: 'conditional',
|
|
51
|
+
test: { kind: 'binary', op: '<', left: { kind: 'identifier', name: 'a' }, right: { kind: 'identifier', name: 'b' } },
|
|
52
|
+
consequent: { kind: 'unary', op: '-', argument: { kind: 'literal', value: 1 } },
|
|
53
|
+
alternate: { kind: 'literal', value: 0 },
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('filter body: logical over member access', () => {
|
|
59
|
+
expect(evalJSON('item.done && item.priority > 3')).toEqual({
|
|
60
|
+
kind: 'logical',
|
|
61
|
+
op: '&&',
|
|
62
|
+
left: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'done' },
|
|
63
|
+
right: {
|
|
64
|
+
kind: 'binary',
|
|
65
|
+
op: '>',
|
|
66
|
+
left: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'priority' },
|
|
67
|
+
right: { kind: 'literal', value: 3 },
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('index-access carries object + index', () => {
|
|
73
|
+
expect(evalJSON('item[i]')).toEqual({
|
|
74
|
+
kind: 'index-access',
|
|
75
|
+
object: { kind: 'identifier', name: 'item' },
|
|
76
|
+
index: { kind: 'identifier', name: 'i' },
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('builtin call carries callee + args', () => {
|
|
81
|
+
expect(evalJSON('Math.max(a, b)')).toEqual({
|
|
82
|
+
kind: 'call',
|
|
83
|
+
callee: { kind: 'member', object: { kind: 'identifier', name: 'Math' }, property: 'max' },
|
|
84
|
+
args: [
|
|
85
|
+
{ kind: 'identifier', name: 'a' },
|
|
86
|
+
{ kind: 'identifier', name: 'b' },
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('template literal: string + expression parts', () => {
|
|
92
|
+
expect(evalJSON('`n=${acc + 1}`')).toEqual({
|
|
93
|
+
kind: 'template-literal',
|
|
94
|
+
parts: [
|
|
95
|
+
{ type: 'string', value: 'n=' },
|
|
96
|
+
{ type: 'expression', expr: { kind: 'binary', op: '+', left: { kind: 'identifier', name: 'acc' }, right: { kind: 'literal', value: 1 } } },
|
|
97
|
+
],
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('map body: object literal carries key + value (no keyKind / shorthand)', () => {
|
|
102
|
+
expect(evalJSON('({ id: item.id, n: item.n })')).toEqual({
|
|
103
|
+
kind: 'object-literal',
|
|
104
|
+
properties: [
|
|
105
|
+
{ key: 'id', value: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'id' } },
|
|
106
|
+
{ key: 'n', value: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'n' } },
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('array literal', () => {
|
|
112
|
+
expect(evalJSON('[item.a, item.b]')).toEqual({
|
|
113
|
+
kind: 'array-literal',
|
|
114
|
+
elements: [
|
|
115
|
+
{ kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'a' },
|
|
116
|
+
{ kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'b' },
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('purity gate: a folded method call in the body → null', () => {
|
|
122
|
+
// `.toUpperCase()` folds to `array-method`, outside the evaluator surface.
|
|
123
|
+
expect(serializeParsedExpr(parseExpression('item.name.toUpperCase()'))).toBeNull()
|
|
124
|
+
// A higher-order call (`.filter`) likewise.
|
|
125
|
+
expect(serializeParsedExpr(parseExpression('item.tags.filter(t => t)'))).toBeNull()
|
|
126
|
+
// An unsupported shape.
|
|
127
|
+
expect(serializeParsedExpr(parseExpression('a instanceof B'))).toBeNull()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('purity gate: a folded subtree anywhere poisons the whole body', () => {
|
|
131
|
+
expect(serializeParsedExpr(parseExpression('acc + item.name.toUpperCase()'))).toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('purity gate: a non-builtin call is refused (evaluator would read it as nil)', () => {
|
|
135
|
+
// Bare function call — not on the evaluator allowlist.
|
|
136
|
+
expect(serializeParsedExpr(parseExpression('foo(item)'))).toBeNull()
|
|
137
|
+
// Method call on a value — generic `call` with a non-Math member callee.
|
|
138
|
+
expect(serializeParsedExpr(parseExpression('item.compute(2)'))).toBeNull()
|
|
139
|
+
// `parseInt` etc. are not the allowlisted `Number`/`String`/`Boolean`.
|
|
140
|
+
expect(serializeParsedExpr(parseExpression('parseInt(item.s)'))).toBeNull()
|
|
141
|
+
// A computed builtin reference the evaluator rejects (`Math['max']`).
|
|
142
|
+
expect(serializeParsedExpr(parseExpression("Math['max'](a, b)"))).toBeNull()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('allowlisted builtins serialize (Math.* / String / Number / Boolean)', () => {
|
|
146
|
+
expect(serializeParsedExpr(parseExpression('Math.floor(item.x)'))).not.toBeNull()
|
|
147
|
+
expect(serializeParsedExpr(parseExpression('String(item.n)'))).not.toBeNull()
|
|
148
|
+
expect(serializeParsedExpr(parseExpression('Number(item.s)'))).not.toBeNull()
|
|
149
|
+
expect(serializeParsedExpr(parseExpression('Boolean(item.s)'))).not.toBeNull()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('a computed member value carries `computed: true` (plain access omits it)', () => {
|
|
153
|
+
// `row['price']` folds to a computed `member`; the flag is preserved so a
|
|
154
|
+
// computed member stays distinguishable. (`row.price` carries no `computed`.)
|
|
155
|
+
expect(evalJSON("row['price']")).toEqual({
|
|
156
|
+
kind: 'member',
|
|
157
|
+
object: { kind: 'identifier', name: 'row' },
|
|
158
|
+
property: 'price',
|
|
159
|
+
computed: true,
|
|
160
|
+
})
|
|
161
|
+
expect(evalJSON('row.price')).toEqual({
|
|
162
|
+
kind: 'member',
|
|
163
|
+
object: { kind: 'identifier', name: 'row' },
|
|
164
|
+
property: 'price',
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('freeVarsInBody', () => {
|
|
170
|
+
test('collects refs minus the callback params, sorted & deduped', () => {
|
|
171
|
+
const body = parseExpression('acc + item.price + acc')
|
|
172
|
+
expect(freeVarsInBody(body, new Set(['acc', 'item']))).toEqual([])
|
|
173
|
+
expect(freeVarsInBody(body, new Set(['acc']))).toEqual(['item'])
|
|
174
|
+
expect(freeVarsInBody(body, new Set())).toEqual(['acc', 'item'])
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('captures an outer free var referenced in the body (base_env source)', () => {
|
|
178
|
+
// `taxRate` is neither param — it must travel as a captured free var.
|
|
179
|
+
const body = parseExpression('acc + item.price * taxRate')
|
|
180
|
+
expect(freeVarsInBody(body, new Set(['acc', 'item']))).toEqual(['taxRate'])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('member property names are not references; object-literal values are', () => {
|
|
184
|
+
// `price` (property) is not a free var; `item` and `factor` are.
|
|
185
|
+
const body = parseExpression('({ total: item.price * factor })')
|
|
186
|
+
expect(freeVarsInBody(body, new Set()).sort()).toEqual(['factor', 'item'])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('template + index + call cover their value positions', () => {
|
|
190
|
+
// `Math` is a builtin callee resolved syntactically by the evaluator — it
|
|
191
|
+
// is NOT captured (it would emit an undefined `$Math` / `.Math` base_env
|
|
192
|
+
// entry). The real refs `k`, `n`, `row` are.
|
|
193
|
+
const body = parseExpression('`${row[k]}-${Math.abs(n)}`')
|
|
194
|
+
expect(freeVarsInBody(body, new Set())).toEqual(['k', 'n', 'row'])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('builtin call callees (Math.<fn> / String / Number / Boolean) are not captured', () => {
|
|
198
|
+
// Each builtin is resolved syntactically by the evaluator, so its
|
|
199
|
+
// identifier must not enter base_env (Copilot review #2031). The
|
|
200
|
+
// arguments, however, ARE real references.
|
|
201
|
+
const body = parseExpression('Math.max(a, factor) + Number(label) + String(x) + Boolean(flag)')
|
|
202
|
+
expect(freeVarsInBody(body, new Set(['a']))).toEqual(['factor', 'flag', 'label', 'x'])
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -216,10 +216,11 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
216
216
|
expect(bf021[0].message).toContain('not a supported shape')
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
test('
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
219
|
+
test('no BF021 for let-inline block-body sort comparator (#2040)', () => {
|
|
220
|
+
// #2040: a value-producing block body (pure `const` bindings + a terminal
|
|
221
|
+
// `return`) normalises to a single expression via let-inline, so a
|
|
222
|
+
// `{ const x = a.price; return x - b.price }` comparator now lowers exactly
|
|
223
|
+
// like the expression-bodied `(a, b) => a.price - b.price`.
|
|
223
224
|
const source = `
|
|
224
225
|
'use client'
|
|
225
226
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -239,6 +240,32 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
239
240
|
const { errors } = compileToIR(source)
|
|
240
241
|
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
241
242
|
|
|
243
|
+
expect(bf021).toHaveLength(0)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('emits BF021 error for imperative block-body sort comparator (#2040)', () => {
|
|
247
|
+
// An imperative comparator (local re-assignment / mutation) has no
|
|
248
|
+
// value-position lowering — `foldBlockToExpr` refuses it, so the arrow stays
|
|
249
|
+
// `unsupported` and the sort extraction surfaces BF021.
|
|
250
|
+
const source = `
|
|
251
|
+
'use client'
|
|
252
|
+
import { createSignal } from '@barefootjs/client'
|
|
253
|
+
|
|
254
|
+
export function TodoList() {
|
|
255
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
256
|
+
return (
|
|
257
|
+
<ul>
|
|
258
|
+
{items().sort((a, b) => { let r = 0; r = a.price - b.price; return r }).map(t => (
|
|
259
|
+
<li>{t.name}</li>
|
|
260
|
+
))}
|
|
261
|
+
</ul>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
`
|
|
265
|
+
|
|
266
|
+
const { errors } = compileToIR(source)
|
|
267
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
268
|
+
|
|
242
269
|
expect(bf021).toHaveLength(1)
|
|
243
270
|
expect(bf021[0].message).toContain('not a supported shape')
|
|
244
271
|
})
|
|
@@ -583,3 +610,70 @@ describe('Rest Pattern in Filter Predicate (BF021, #1532)', () => {
|
|
|
583
610
|
expect(bf021).toHaveLength(0)
|
|
584
611
|
})
|
|
585
612
|
})
|
|
613
|
+
|
|
614
|
+
// Block-bodied filter predicates are normalized to a single boolean expression
|
|
615
|
+
// (#2040). A value-producing block lowers like an expression predicate; an
|
|
616
|
+
// imperative block refuses.
|
|
617
|
+
describe('Block-body filter predicate normalization (#2040)', () => {
|
|
618
|
+
function loopFilterIR(predicate: string) {
|
|
619
|
+
const source = `
|
|
620
|
+
'use client'
|
|
621
|
+
import { createSignal } from '@barefootjs/client'
|
|
622
|
+
|
|
623
|
+
export function TodoList() {
|
|
624
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
625
|
+
const [filter, setFilter] = createSignal('all')
|
|
626
|
+
return (
|
|
627
|
+
<ul>
|
|
628
|
+
{items().filter(${predicate}).map(t => (
|
|
629
|
+
<li>{t.name}</li>
|
|
630
|
+
))}
|
|
631
|
+
</ul>
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
`
|
|
635
|
+
const { ir, errors } = compileToIR(source)
|
|
636
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
637
|
+
// Find the loop node carrying the filterPredicate.
|
|
638
|
+
let found: any = null
|
|
639
|
+
const walk = (n: any) => {
|
|
640
|
+
if (!n || found) return
|
|
641
|
+
if (n.filterPredicate) found = n
|
|
642
|
+
for (const c of n.children ?? []) walk(c)
|
|
643
|
+
}
|
|
644
|
+
walk(ir)
|
|
645
|
+
return { bf021, filterPredicate: found?.filterPredicate }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
test('value-producing block (let-inline + early return) folds to a predicate', () => {
|
|
649
|
+
const { bf021, filterPredicate } = loopFilterIR(`t => {
|
|
650
|
+
const f = filter()
|
|
651
|
+
if (f === 'active') return !t.done
|
|
652
|
+
if (f === 'completed') return t.done
|
|
653
|
+
return true
|
|
654
|
+
}`)
|
|
655
|
+
expect(bf021).toHaveLength(0)
|
|
656
|
+
// No leftover block shape — a single boolean predicate expression.
|
|
657
|
+
expect(filterPredicate?.predicate).toBeDefined()
|
|
658
|
+
expect((filterPredicate as any)?.blockBody).toBeUndefined()
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
test('signal read on multiple branches still folds (idempotent getter is pure)', () => {
|
|
662
|
+
const { bf021, filterPredicate } = loopFilterIR(`t => {
|
|
663
|
+
const f = filter()
|
|
664
|
+
if (f === 'active') return !t.done
|
|
665
|
+
return f === 'completed' ? t.done : true
|
|
666
|
+
}`)
|
|
667
|
+
expect(bf021).toHaveLength(0)
|
|
668
|
+
expect(filterPredicate?.predicate).toBeDefined()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
test('imperative block (local re-assignment) refuses with BF021', () => {
|
|
672
|
+
const { bf021 } = loopFilterIR(`t => {
|
|
673
|
+
let keep = false
|
|
674
|
+
keep = !t.done
|
|
675
|
+
return keep
|
|
676
|
+
}`)
|
|
677
|
+
expect(bf021.length).toBeGreaterThan(0)
|
|
678
|
+
})
|
|
679
|
+
})
|
|
@@ -10,41 +10,80 @@ import type { ParsedExpr } from '../expression-parser.ts'
|
|
|
10
10
|
import type { IRMetadata } from '../types.ts'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
13
|
+
* Env-signal key → the runtime factory that produces it (`'search'` →
|
|
14
|
+
* `createSearchParams`). Single source of truth for the reverse of the
|
|
15
|
+
* analyzer's `ENV_SIGNAL_FACTORIES` (#2057): an env signal is `createSignal`-
|
|
16
|
+
* shaped for analysis, but every backend that re-emits its declaration (client
|
|
17
|
+
* JS, JSX/Hono SSR) must call this factory, not `createSignal`, so the value is
|
|
18
|
+
* the request-scoped reader rather than stored state.
|
|
19
|
+
*/
|
|
20
|
+
export const ENV_SIGNAL_CLIENT_FACTORY: Record<string, string> = {
|
|
21
|
+
search: 'createSearchParams',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The getter name(s) of the `searchParams` env signal in this component.
|
|
26
|
+
*
|
|
27
|
+
* Recognised **structurally** (#2057): the env signal is now declared as a
|
|
28
|
+
* `createSignal`-shaped `const [searchParams, setSearchParams] =
|
|
29
|
+
* createSearchParams()`, so the analyzer collects it into `metadata.signals`
|
|
30
|
+
* with `envReader: 'search'` — exactly like any other signal, but tagged. This
|
|
31
|
+
* function returns those getters (whatever the destructured name is —
|
|
32
|
+
* `searchParams`, or an alias), so adapters match the reader `.get()` call
|
|
33
|
+
* against the binding actually used, with **no `searchParams`-name allow-list**
|
|
34
|
+
* (this supersedes the import-name matching, and the closed #2055).
|
|
20
35
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 'searchParams'` and the local binding is `alias ?? name`. Namespace / default
|
|
24
|
-
* specifiers bind a different identifier and are excluded.
|
|
36
|
+
* Empty when the component declares no env signal (the component keeps the
|
|
37
|
+
* generic signal lowering).
|
|
25
38
|
*/
|
|
26
39
|
export function searchParamsLocalNames(metadata: IRMetadata): Set<string> {
|
|
27
40
|
const names = new Set<string>()
|
|
28
|
-
for (const
|
|
29
|
-
if (
|
|
30
|
-
for (const s of imp.specifiers) {
|
|
31
|
-
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
32
|
-
if (s.name === 'searchParams') names.add(s.alias ?? s.name)
|
|
33
|
-
}
|
|
41
|
+
for (const s of metadata.signals) {
|
|
42
|
+
if (s.envReader === 'search') names.add(s.getter)
|
|
34
43
|
}
|
|
35
44
|
return names
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
* True when the component
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
48
|
+
* True when the component declares the `searchParams` env signal. Convenience
|
|
49
|
+
* for adapters/harnesses that only need to gate on presence (the lowering
|
|
50
|
+
* itself needs the {@link searchParamsLocalNames} set to match the actual
|
|
51
|
+
* binding in the expression).
|
|
43
52
|
*/
|
|
44
53
|
export function importsSearchParams(metadata: IRMetadata): boolean {
|
|
45
54
|
return searchParamsLocalNames(metadata).size > 0
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The local binding name(s) that `queryHref` is imported under in this component
|
|
59
|
+
* (#2042) — the pure URL-query builder an adapter lowers to its query helper
|
|
60
|
+
* (`bf_query` in go-template). Matched by the exported name `queryHref` and bound
|
|
61
|
+
* to `alias ?? name`, so an aliased import (`import { queryHref as qh }`) is gated
|
|
62
|
+
* against the LOCAL name. Empty when not imported.
|
|
63
|
+
*
|
|
64
|
+
* Both the main `@barefootjs/client` entry and the `@barefootjs/client/runtime`
|
|
65
|
+
* re-export are accepted: `queryHref` is exported from both, so importing it from
|
|
66
|
+
* either must enable SSR lowering — otherwise the call's object-literal arg would
|
|
67
|
+
* hit the support gate (BF101) on the runtime-entry import path.
|
|
68
|
+
*/
|
|
69
|
+
export function queryHrefLocalNames(metadata: IRMetadata): Set<string> {
|
|
70
|
+
const names = new Set<string>()
|
|
71
|
+
for (const imp of metadata.imports) {
|
|
72
|
+
if (!QUERY_HREF_SOURCES.has(imp.source) || imp.isTypeOnly) continue
|
|
73
|
+
for (const s of imp.specifiers) {
|
|
74
|
+
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
75
|
+
if (s.name === 'queryHref') names.add(s.alias ?? s.name)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return names
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Entry points that re-export `queryHref` (main + the runtime re-export). */
|
|
82
|
+
const QUERY_HREF_SOURCES: ReadonlySet<string> = new Set([
|
|
83
|
+
'@barefootjs/client',
|
|
84
|
+
'@barefootjs/client/runtime',
|
|
85
|
+
])
|
|
86
|
+
|
|
48
87
|
/**
|
|
49
88
|
* Recognise a `<binding>().<method>(<args>)` env-signal method call from a
|
|
50
89
|
* `call` node's callee + args, where `<binding>` is one of the local names
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from '../types.ts'
|
|
13
13
|
import { BF_SCOPE, BF_SLOT, BF_COND } from '@barefootjs/shared'
|
|
14
14
|
import { BaseAdapter } from './interface.ts'
|
|
15
|
+
import { ENV_SIGNAL_CLIENT_FACTORY } from './env-signal.ts'
|
|
15
16
|
import { formatParamWithType, findReachableNames } from '../module-exports.ts'
|
|
16
17
|
|
|
17
18
|
export interface JsxAdapterConfig {
|
|
@@ -110,6 +111,22 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
110
111
|
|
|
111
112
|
for (const signal of ir.metadata.signals) {
|
|
112
113
|
if (signal.isModule) continue
|
|
114
|
+
if (signal.envReader) {
|
|
115
|
+
// Env signal (#2057): call the real runtime factory so SSR resolves the
|
|
116
|
+
// request query through the installed server env reader, not a static
|
|
117
|
+
// initial value. Emit the factory as written (alias / namespace aware),
|
|
118
|
+
// matching the import re-emitted into the SSR module; fall back to the
|
|
119
|
+
// canonical name if the callee text wasn't captured.
|
|
120
|
+
const factory = signal.envFactory ?? ENV_SIGNAL_CLIENT_FACTORY[signal.envReader]
|
|
121
|
+
if (factory) {
|
|
122
|
+
lines.push(
|
|
123
|
+
signal.setter
|
|
124
|
+
? ` const [${signal.getter}, ${signal.setter}] = ${factory}()`
|
|
125
|
+
: ` const [${signal.getter}] = ${factory}()`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
113
130
|
// Create a getter that returns the initial value for SSR
|
|
114
131
|
const rawInitialValue = preserveTypes
|
|
115
132
|
? (signal.typedInitialValue ?? signal.initialValue)
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
* be added in one place.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import type { ParsedExpr,
|
|
36
|
+
import type { ParsedExpr, FlatDepth, TemplatePart, ObjectLiteralProperty } from '../expression-parser.ts'
|
|
37
|
+
import { asCallbackMethodCall } from '../expression-parser.ts'
|
|
37
38
|
|
|
38
39
|
export type HigherOrderMethod = 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex'
|
|
39
40
|
|
|
@@ -139,49 +140,48 @@ export interface ParsedExprEmitter {
|
|
|
139
140
|
emit: (e: ParsedExpr) => string,
|
|
140
141
|
): string
|
|
141
142
|
templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// A higher-order callback method call (`<object>.<method>(<arrow>, …rest)`,
|
|
144
|
+
// method ∈ `CALLBACK_METHODS`): `.filter`/`.find`/`.every`/`.some`/`.sort`/
|
|
145
|
+
// `.reduce`/`.flatMap`/… (#2018 P5). The adapter serializes the `arrow` body
|
|
146
|
+
// to the runtime evaluator (eval-first) and falls back to a structured
|
|
147
|
+
// lowering when the body is outside the evaluator surface. `restArgs` carries
|
|
148
|
+
// any trailing arguments (e.g. the `.reduce` init).
|
|
149
|
+
callbackMethod(
|
|
150
|
+
method: string,
|
|
145
151
|
object: ParsedExpr,
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
|
|
153
|
+
restArgs: ParsedExpr[],
|
|
148
154
|
emit: (e: ParsedExpr) => string,
|
|
149
155
|
): string
|
|
156
|
+
// A standalone arrow / regex literal. These normally reach an adapter only as
|
|
157
|
+
// a callback argument (handled by `callbackMethod`); emitted standalone they
|
|
158
|
+
// have no template form, so adapters route them to their `unsupported` path.
|
|
159
|
+
arrow(params: string[], body: ParsedExpr, emit: (e: ParsedExpr) => string): string
|
|
160
|
+
regex(raw: string): string
|
|
150
161
|
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string
|
|
162
|
+
// Emit an object literal `{ a: 1, b: x }`. `raw` is the original
|
|
163
|
+
// expression string so an adapter that doesn't lower object values yet
|
|
164
|
+
// can delegate to `unsupported(raw, …)` and stay byte-identical.
|
|
165
|
+
objectLiteral(
|
|
166
|
+
properties: ObjectLiteralProperty[],
|
|
167
|
+
raw: string,
|
|
168
|
+
emit: (e: ParsedExpr) => string,
|
|
169
|
+
): string
|
|
151
170
|
arrayMethod(
|
|
152
171
|
method: ArrayMethod,
|
|
153
172
|
object: ParsedExpr,
|
|
154
173
|
args: ParsedExpr[],
|
|
155
174
|
emit: (e: ParsedExpr) => string,
|
|
156
175
|
): string
|
|
157
|
-
sortMethod(
|
|
158
|
-
method: SortMethod,
|
|
159
|
-
object: ParsedExpr,
|
|
160
|
-
comparator: SortComparator,
|
|
161
|
-
emit: (e: ParsedExpr) => string,
|
|
162
|
-
): string
|
|
163
|
-
reduceMethod(
|
|
164
|
-
method: ReduceMethod,
|
|
165
|
-
object: ParsedExpr,
|
|
166
|
-
reduceOp: ReduceOp,
|
|
167
|
-
emit: (e: ParsedExpr) => string,
|
|
168
|
-
): string
|
|
169
176
|
// `.flat(depth?)` gets its own dispatcher arm (#1448 Tier C): it carries
|
|
170
177
|
// a structured `FlatDepth` (the validated literal / `'infinity'`) rather
|
|
171
|
-
// than a `ParsedExpr[]` args list,
|
|
178
|
+
// than a `ParsedExpr[]` args list. Non-callback, so it is NOT routed
|
|
179
|
+
// through `callbackMethod`.
|
|
172
180
|
flatMethod(
|
|
173
181
|
object: ParsedExpr,
|
|
174
182
|
depth: FlatDepth,
|
|
175
183
|
emit: (e: ParsedExpr) => string,
|
|
176
184
|
): string
|
|
177
|
-
// `.flatMap(fn)` value-returning field projection gets its own arm
|
|
178
|
-
// (#1448 Tier C): it carries a structured `FlatMapOp` rather than a
|
|
179
|
-
// `ParsedExpr[]` args list, same rationale as sort / reduce / flat.
|
|
180
|
-
flatMapMethod(
|
|
181
|
-
object: ParsedExpr,
|
|
182
|
-
op: FlatMapOp,
|
|
183
|
-
emit: (e: ParsedExpr) => string,
|
|
184
|
-
): string
|
|
185
185
|
unsupported(raw: string, reason: string): string
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -201,8 +201,13 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
201
201
|
return emitter.identifier(expr.name)
|
|
202
202
|
case 'literal':
|
|
203
203
|
return emitter.literal(expr.value, expr.literalType)
|
|
204
|
-
case 'call':
|
|
204
|
+
case 'call': {
|
|
205
|
+
// A higher-order callback call (`arr.filter(p)` / `arr.sort(cmp)` / …)
|
|
206
|
+
// routes to the dedicated `callbackMethod` arm; any other call is generic.
|
|
207
|
+
const cb = asCallbackMethodCall(expr)
|
|
208
|
+
if (cb) return emitter.callbackMethod(cb.method, cb.object, cb.arrow, cb.args, emit)
|
|
205
209
|
return emitter.call(expr.callee, expr.args, emit)
|
|
210
|
+
}
|
|
206
211
|
case 'member':
|
|
207
212
|
return emitter.member(expr.object, expr.property, expr.computed, emit)
|
|
208
213
|
case 'index-access':
|
|
@@ -217,25 +222,18 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
217
222
|
return emitter.conditional(expr.test, expr.consequent, expr.alternate, emit)
|
|
218
223
|
case 'template-literal':
|
|
219
224
|
return emitter.templateLiteral(expr.parts, emit)
|
|
220
|
-
case 'arrow
|
|
221
|
-
return emitter.
|
|
222
|
-
case '
|
|
223
|
-
return emitter.
|
|
225
|
+
case 'arrow':
|
|
226
|
+
return emitter.arrow(expr.params, expr.body, emit)
|
|
227
|
+
case 'regex':
|
|
228
|
+
return emitter.regex(expr.raw)
|
|
224
229
|
case 'array-literal':
|
|
225
230
|
return emitter.arrayLiteral(expr.elements, emit)
|
|
231
|
+
case 'object-literal':
|
|
232
|
+
return emitter.objectLiteral(expr.properties, expr.raw, emit)
|
|
226
233
|
case 'array-method':
|
|
227
|
-
if (expr.method === 'sort' || expr.method === 'toSorted') {
|
|
228
|
-
return emitter.sortMethod(expr.method, expr.object, expr.comparator, emit)
|
|
229
|
-
}
|
|
230
|
-
if (expr.method === 'reduce' || expr.method === 'reduceRight') {
|
|
231
|
-
return emitter.reduceMethod(expr.method, expr.object, expr.reduceOp, emit)
|
|
232
|
-
}
|
|
233
234
|
if (expr.method === 'flat') {
|
|
234
235
|
return emitter.flatMethod(expr.object, expr.flatDepth, emit)
|
|
235
236
|
}
|
|
236
|
-
if (expr.method === 'flatMap') {
|
|
237
|
-
return emitter.flatMapMethod(expr.object, expr.flatMapOp, emit)
|
|
238
|
-
}
|
|
239
237
|
return emitter.arrayMethod(expr.method, expr.object, expr.args, emit)
|
|
240
238
|
case 'unsupported':
|
|
241
239
|
return emitter.unsupported(expr.raw, expr.reason)
|