@barefootjs/jinja 0.1.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/README.md +66 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +84 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +106 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189097 -0
- package/dist/adapter/jinja-adapter.d.ts +394 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +21 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/jinja-naming.d.ts +77 -0
- package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +81 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +61 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189117 -0
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189118 -0
- package/package.json +66 -0
- package/python/VERSION +1 -0
- package/python/barefootjs/__init__.py +57 -0
- package/python/barefootjs/backend_jinja.py +125 -0
- package/python/barefootjs/evaluator.py +787 -0
- package/python/barefootjs/runtime.py +1334 -0
- package/python/barefootjs/search_params.py +89 -0
- package/python/pyproject.toml +32 -0
- package/python/tests/__init__.py +0 -0
- package/python/tests/test_eval_vectors.py +88 -0
- package/python/tests/test_evaluator.py +406 -0
- package/python/tests/test_helper_vectors.py +288 -0
- package/python/tests/test_omit.py +62 -0
- package/python/tests/test_props_attr.py +54 -0
- package/python/tests/test_query.py +41 -0
- package/python/tests/test_render.py +102 -0
- package/python/tests/test_render_child.py +96 -0
- package/python/tests/test_search_params.py +50 -0
- package/python/tests/test_spread_attrs.py +86 -0
- package/python/tests/test_template_primitives.py +347 -0
- package/python/tests/vector-divergences.json +42 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
- package/src/__tests__/jinja-adapter.test.ts +53 -0
- package/src/__tests__/jinja-counter.test.ts +62 -0
- package/src/__tests__/jinja-query-href.test.ts +99 -0
- package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +176 -0
- package/src/adapter/emit-context.ts +118 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/jinja-adapter.ts +1747 -0
- package/src/adapter/lib/constants.ts +33 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/jinja-naming.ts +114 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +132 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +166 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +714 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ported from the Xslate-specific describes in
|
|
3
|
+
* `packages/adapter-xslate/src/__tests__/xslate-adapter.test.ts` (lines
|
|
4
|
+
* 152-400): SSR context propagation, memo seeding, signal seeding +
|
|
5
|
+
* data-key, #1966 @client attribute deferral, #2073 map_eval, #2018 P2
|
|
6
|
+
* predicate lowering, #2075 searchParams()-derived memo seeding. Expected
|
|
7
|
+
* template strings are translated to Jinja syntax. `runAdapterConformanceTests`
|
|
8
|
+
* itself (lines 24-150) and `src/test-render.ts` are workstream C and are NOT
|
|
9
|
+
* ported here.
|
|
10
|
+
*
|
|
11
|
+
* One describe (`prop-derived signal SSR seeding + data-key`) diverges
|
|
12
|
+
* behaviourally, not just syntactically: the "does NOT in-template-seed a
|
|
13
|
+
* same-name signal" Xslate test is inverted here to "DOES seed" — see
|
|
14
|
+
* `memo/seed.ts`'s file header for why Jinja's `{% set %}` doesn't have
|
|
15
|
+
* Kolon's `my`-shadowing hazard.
|
|
16
|
+
*
|
|
17
|
+
* The "#2075 searchParams()-derived memo seeding" describe also diverges: the
|
|
18
|
+
* "filter memo chained off the derived memo" case asserts a `bf.filter_eval(`
|
|
19
|
+
* seed (not Xslate's `$bf.filter(` lambda-closure), and asserts the
|
|
20
|
+
* evaluator's captured env dict (`{'tag': tag}`) rather than a bare `$tag`
|
|
21
|
+
* lambda-body reference — see `memo/seed.ts`'s file header on
|
|
22
|
+
* `materializeGetterCalls` (divergence 3: Jinja has no lambda fallback, so a
|
|
23
|
+
* sibling-getter call inside a predicate must materialize to a free var the
|
|
24
|
+
* evaluator can capture, instead of closing over the lexical directly).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, test, expect } from 'bun:test'
|
|
28
|
+
import { JinjaAdapter } from '../adapter'
|
|
29
|
+
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Helpers
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
function compileToIR(source: string): ComponentIR {
|
|
36
|
+
const result = compileJSX(source.trimStart(), 'test.tsx', {
|
|
37
|
+
adapter: new JinjaAdapter(),
|
|
38
|
+
outputIR: true,
|
|
39
|
+
})
|
|
40
|
+
const irFile = result.files.find(f => f.type === 'ir')
|
|
41
|
+
if (!irFile) throw new Error('No IR output')
|
|
42
|
+
return JSON.parse(irFile.content) as ComponentIR
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compileAndGenerate(source: string) {
|
|
46
|
+
return new JinjaAdapter().generate(compileToIR(source))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Jinja-Specific Tests
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
describe('JinjaAdapter - SSR context propagation (#1297)', () => {
|
|
54
|
+
// `<Ctx.Provider value>` brackets its children with inline provide/revoke
|
|
55
|
+
// calls (both return '' so the `{{ … }}` interpolation discards them);
|
|
56
|
+
// descendant `useContext` consumers read the value during the same
|
|
57
|
+
// render.
|
|
58
|
+
test('provider brackets children with provide_context / revoke_context', () => {
|
|
59
|
+
const { template } = compileAndGenerate(`
|
|
60
|
+
'use client'
|
|
61
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
62
|
+
const ThemeContext = createContext('light')
|
|
63
|
+
export function ThemeRoot() {
|
|
64
|
+
return <div><ThemeContext.Provider value="dark"><ThemeLabel /></ThemeContext.Provider></div>
|
|
65
|
+
}
|
|
66
|
+
function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
|
|
67
|
+
`)
|
|
68
|
+
expect(template).toContain("bf.provide_context('ThemeContext', 'dark')")
|
|
69
|
+
expect(template).toContain("bf.revoke_context('ThemeContext')")
|
|
70
|
+
expect(template.indexOf('provide_context')).toBeLessThan(template.indexOf('render_child'))
|
|
71
|
+
expect(template.indexOf('render_child')).toBeLessThan(template.indexOf('revoke_context'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('consumer seeds its local from use_context with the createContext default', () => {
|
|
75
|
+
const { template } = compileAndGenerate(`
|
|
76
|
+
'use client'
|
|
77
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
78
|
+
const ThemeContext = createContext('light')
|
|
79
|
+
export function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
|
|
80
|
+
`)
|
|
81
|
+
expect(template).toContain("{% set theme = bf.use_context('ThemeContext', 'light') %}")
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('JinjaAdapter - prop-derived memo SSR seeding (#1297)', () => {
|
|
86
|
+
// A memo whose body can't be statically folded (`props.value * 10`) gets a
|
|
87
|
+
// `null` SSR default; the adapter computes it in-template from the seeded
|
|
88
|
+
// prop var so the child renders the value instead of empty.
|
|
89
|
+
test('seeds a prop-derived memo from the prop var', () => {
|
|
90
|
+
const { template } = compileAndGenerate(`
|
|
91
|
+
'use client'
|
|
92
|
+
import { createMemo } from '@barefootjs/client'
|
|
93
|
+
export function Child(props: { value: number }) {
|
|
94
|
+
const displayValue = createMemo(() => props.value * 10)
|
|
95
|
+
return <span>{displayValue()}</span>
|
|
96
|
+
}
|
|
97
|
+
`)
|
|
98
|
+
expect(template).toContain('{% set displayValue = value * 10 %}')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('seeds a memo over a destructured prop', () => {
|
|
102
|
+
const { template } = compileAndGenerate(`
|
|
103
|
+
'use client'
|
|
104
|
+
import { createMemo } from '@barefootjs/client'
|
|
105
|
+
export function Child({ value }: { value: number }) {
|
|
106
|
+
const displayValue = createMemo(() => value * 10)
|
|
107
|
+
return <span>{displayValue()}</span>
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
expect(template).toContain('{% set displayValue = value * 10 %}')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('JinjaAdapter - prop-derived signal SSR seeding + data-key (#1297, toggle-shared)', () => {
|
|
115
|
+
test('seeds a prop-derived (different-name) signal from the prop var', () => {
|
|
116
|
+
const { template } = compileAndGenerate(`
|
|
117
|
+
'use client'
|
|
118
|
+
import { createSignal } from '@barefootjs/client'
|
|
119
|
+
export function Item(props: { defaultOn?: boolean }) {
|
|
120
|
+
const [on, setOn] = createSignal(props.defaultOn ?? false)
|
|
121
|
+
return <button>{on() ? 'ON' : 'OFF'}</button>
|
|
122
|
+
}
|
|
123
|
+
`)
|
|
124
|
+
expect(template).toContain('{% set on = (defaultOn if (defaultOn is defined and defaultOn is not none) else false) %}')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Divergence from the Kolon port: Jinja's `{% set x = x + 1 %}` safely
|
|
128
|
+
// resolves the right-hand `x` from the enclosing scope (no `my`-shadowing
|
|
129
|
+
// hazard), so a same-name signal IS seeded in-template here — strictly
|
|
130
|
+
// more correct than leaving it on the static default. See
|
|
131
|
+
// `memo/seed.ts`'s file header.
|
|
132
|
+
test('DOES in-template-seed a same-name signal (no Kolon `my`-shadow hazard in Jinja)', () => {
|
|
133
|
+
const { template } = compileAndGenerate(`
|
|
134
|
+
'use client'
|
|
135
|
+
import { createSignal } from '@barefootjs/client'
|
|
136
|
+
export function C(props: { x?: number }) {
|
|
137
|
+
const [x, setX] = createSignal(props.x ?? 7)
|
|
138
|
+
return <span>{x()}</span>
|
|
139
|
+
}
|
|
140
|
+
`)
|
|
141
|
+
expect(template).toContain('{% set x = (x if (x is defined and x is not none) else 7) %}')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('emits data_key_attr on the component root', () => {
|
|
145
|
+
const { template } = compileAndGenerate(`
|
|
146
|
+
export function Item() { return <div class="x">hi</div> }
|
|
147
|
+
`)
|
|
148
|
+
expect(template).toContain('bf.data_key_attr()')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('emits data_key_attr on each branch root of an if-statement root', () => {
|
|
152
|
+
const { template } = compileAndGenerate(`
|
|
153
|
+
export function Item({ on }: { on?: boolean }) {
|
|
154
|
+
if (on) return <div class="a">A</div>
|
|
155
|
+
return <div class="b">B</div>
|
|
156
|
+
}
|
|
157
|
+
`)
|
|
158
|
+
const count = (template.match(/bf\.data_key_attr\(\)/g) ?? []).length
|
|
159
|
+
expect(count).toBe(2)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// =============================================================================
|
|
164
|
+
// #1966 — `/* @client */` defers ATTRIBUTE bindings (not just child/text)
|
|
165
|
+
// =============================================================================
|
|
166
|
+
//
|
|
167
|
+
// `renderAttributes` skips SSR emission for `attr.clientOnly`, so a
|
|
168
|
+
// deferred attribute predicate is omitted from the Jinja template (and the
|
|
169
|
+
// unsupported-expression lowering is never reached → no BF101/BF102). The
|
|
170
|
+
// client runtime sets the attribute on hydrate. Mirrors the Go / Xslate pins.
|
|
171
|
+
describe('JinjaAdapter - #1966 @client defers attribute bindings', () => {
|
|
172
|
+
function compileAttr(attrExpr: string) {
|
|
173
|
+
const adapter = new JinjaAdapter()
|
|
174
|
+
const ir = compileToIR(`
|
|
175
|
+
"use client"
|
|
176
|
+
import { createSignal } from "@barefootjs/client"
|
|
177
|
+
export function C() {
|
|
178
|
+
const [sel] = createSignal(0)
|
|
179
|
+
const pred = (n: number) => sel() === n
|
|
180
|
+
return <div data-x={${attrExpr}}>hi</div>
|
|
181
|
+
}
|
|
182
|
+
`)
|
|
183
|
+
const template = adapter.generate(ir).template ?? ''
|
|
184
|
+
const errors = (adapter as unknown as { errors: { code: string }[] }).errors ?? []
|
|
185
|
+
return { errors, template }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
test('bare emits data-x; @client omits it from SSR', () => {
|
|
189
|
+
expect(compileAttr('pred(1)').template).toContain('data-x')
|
|
190
|
+
const deferred = compileAttr('/* @client */ pred(1)')
|
|
191
|
+
expect(deferred.errors).toEqual([])
|
|
192
|
+
expect(deferred.template).not.toContain('data-x')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// #2018 P2: higher-order predicates lower through the runtime evaluator
|
|
197
|
+
// (`bf.*_eval`), isomorphic with the Go / Xslate `*_eval` helpers. Jinja has
|
|
198
|
+
// no lambda fallback (divergence 3 in `jinja-adapter.ts`'s header) — a
|
|
199
|
+
// predicate the evaluator can't model surfaces BF101 instead.
|
|
200
|
+
describe('JinjaAdapter - #2073 value-producing .map(cb)', () => {
|
|
201
|
+
// The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
|
|
202
|
+
// projection, not JSX) lowers through the evaluator — `bf.map_eval`
|
|
203
|
+
// projects each element (no flatten) and composes through `bf.join`.
|
|
204
|
+
test('.map(t => `#${t}`).join(" ") emits bf.map_eval composed into bf.join', () => {
|
|
205
|
+
const { template } = compileAndGenerate(`
|
|
206
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
207
|
+
return <p>{tags.map((t) => \`#\${t}\`).join(' ')}</p>
|
|
208
|
+
}
|
|
209
|
+
export { TagLine }
|
|
210
|
+
`)
|
|
211
|
+
expect(template).toContain("bf.join(bf.map_eval(tags,")
|
|
212
|
+
expect(template).toContain('"kind":"template-literal"')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('.map(u => u.name) emits bf.map_eval with the field projection', () => {
|
|
216
|
+
const { template } = compileAndGenerate(`
|
|
217
|
+
function NameList({ users }: { users: { name: string }[] }) {
|
|
218
|
+
return <div>{users.map((u) => u.name).join(', ')}</div>
|
|
219
|
+
}
|
|
220
|
+
export { NameList }
|
|
221
|
+
`)
|
|
222
|
+
expect(template).toContain('bf.map_eval(users,')
|
|
223
|
+
expect(template).toContain('"property":"name"')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('JinjaAdapter - higher-order predicate lowering (#2018 P2)', () => {
|
|
228
|
+
test('a serializable predicate lowers to bf.filter_eval with the JSON body + env', () => {
|
|
229
|
+
// A standalone `.filter().length` exercises the higher-order emitter (the
|
|
230
|
+
// `.filter().map()` form is a loop-hoist with an inline `{% if %}`,
|
|
231
|
+
// handled by renderLoop, not this emitter).
|
|
232
|
+
const { template } = compileAndGenerate(`
|
|
233
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
234
|
+
return <div>{items.filter(x => x.done).length}</div>
|
|
235
|
+
}
|
|
236
|
+
export { A }
|
|
237
|
+
`)
|
|
238
|
+
expect(template).toContain('bf.filter_eval(')
|
|
239
|
+
expect(template).toContain('"property":"done"')
|
|
240
|
+
expect(template).toContain("'x'")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('.find / .findLast share bf.find_eval, distinguished by the forward flag', () => {
|
|
244
|
+
const find = compileAndGenerate(`
|
|
245
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
246
|
+
return <div>{items.find(x => x.done) ? 'y' : 'n'}</div>
|
|
247
|
+
}
|
|
248
|
+
export { A }
|
|
249
|
+
`).template
|
|
250
|
+
expect(find).toContain('bf.find_eval(')
|
|
251
|
+
// `forward` is a genuine Python bool literal here (`true`/`false`),
|
|
252
|
+
// unlike the Kolon port's Perl `1`/`0` — see `expr/array-method.ts`.
|
|
253
|
+
expect(find).toContain(", true, {})")
|
|
254
|
+
|
|
255
|
+
const findLast = compileAndGenerate(`
|
|
256
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
257
|
+
return <div>{items.findLast(x => x.done) ? 'y' : 'n'}</div>
|
|
258
|
+
}
|
|
259
|
+
export { A }
|
|
260
|
+
`).template
|
|
261
|
+
expect(findLast).toContain('bf.find_eval(')
|
|
262
|
+
expect(findLast).toContain(", false, {})")
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// #2075: `.includes(x)` joined the evaluator's `array-method` surface
|
|
266
|
+
// (shared with the Perl/Go evaluator runtimes), so a predicate built from
|
|
267
|
+
// it now routes through `bf.every_eval` like any other pure predicate —
|
|
268
|
+
// no BF101, despite being a method-call predicate.
|
|
269
|
+
test('.includes() in a predicate now lowers via the evaluator, not a BF101 refusal', () => {
|
|
270
|
+
const { template } = compileAndGenerate(`
|
|
271
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
272
|
+
return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
|
|
273
|
+
}
|
|
274
|
+
export { A }
|
|
275
|
+
`)
|
|
276
|
+
expect(template).toContain('bf.every_eval(')
|
|
277
|
+
expect(template).toContain('"method":"includes"')
|
|
278
|
+
expect(template).not.toContain('bf.truthy(\'\')')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Divergence 3 (`jinja-adapter.ts`'s header): Jinja has no lambda
|
|
282
|
+
// expression, so a predicate the evaluator can't serialize (a method call
|
|
283
|
+
// outside the evaluator's `array-method` gate — only `includes` is
|
|
284
|
+
// recognized there) has NO fallback — unlike Kolon, which falls back to a
|
|
285
|
+
// `-> $x { … }` lambda passed to `$bf.every`. This surfaces BF101 instead.
|
|
286
|
+
test('a method-call predicate outside the evaluator surface has no lambda fallback — surfaces BF101', () => {
|
|
287
|
+
const adapter = new JinjaAdapter()
|
|
288
|
+
const ir = compileToIR(`
|
|
289
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
290
|
+
return <div>{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}</div>
|
|
291
|
+
}
|
|
292
|
+
export { A }
|
|
293
|
+
`)
|
|
294
|
+
const { template } = adapter.generate(ir)
|
|
295
|
+
const errors = (adapter as unknown as { errors: { code: string; message: string }[] }).errors
|
|
296
|
+
expect(errors.some(e => e.code === 'BF101' && e.message.includes("'.every(...)'"))).toBe(true)
|
|
297
|
+
// No evaluator helper, and — critically — no lambda syntax either;
|
|
298
|
+
// the condition falls back to the safe `bf.truthy('')` empty-string
|
|
299
|
+
// sentinel (see `convertExpressionToJinja`'s BF101 path).
|
|
300
|
+
expect(template).not.toContain('every_eval')
|
|
301
|
+
expect(template).not.toContain('bf.every(')
|
|
302
|
+
expect(template).toContain("bf.truthy('')")
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// #2075: derived signal/memo seeding now consumes the shared
|
|
307
|
+
// `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts) instead of
|
|
308
|
+
// re-deriving scope/support locally — same plan Xslate/Mojo consume, ported
|
|
309
|
+
// to `{% set %}` syntax. Mirrors
|
|
310
|
+
// `packages/adapter-xslate/src/__tests__/xslate-adapter.test.ts`'s
|
|
311
|
+
// "#2075 searchParams()-derived memo seeding" describe.
|
|
312
|
+
describe('JinjaAdapter - #2075 searchParams()-derived memo seeding', () => {
|
|
313
|
+
test('seeds an aliased scalar derived memo from the canonical reader', () => {
|
|
314
|
+
const { template } = compileAndGenerate(`
|
|
315
|
+
'use client'
|
|
316
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
317
|
+
export function SortStatus() {
|
|
318
|
+
const [sp] = createSearchParams()
|
|
319
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
320
|
+
return <p>sort: {sort()}</p>
|
|
321
|
+
}
|
|
322
|
+
`)
|
|
323
|
+
expect(template).toContain(
|
|
324
|
+
"{% set sort = (searchParams.get('sort') if (searchParams.get('sort') is defined and searchParams.get('sort') is not none) else 'date') %}",
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// The evaluator-JSON `param` and the `bf` runtime object are
|
|
329
|
+
// lowering-internal, not out-of-scope template vars (#2075). Jinja routes
|
|
330
|
+
// the filter predicate through `bf.filter_eval` (divergence 3 — no lambda
|
|
331
|
+
// fallback), materializing the sibling getter call `tag()` into a bare
|
|
332
|
+
// free-var read (`materializeGetterCalls`) so the evaluator can serialize
|
|
333
|
+
// it at all; the captured env then resolves against the `tag` seed line
|
|
334
|
+
// emitted just above (see `memo/seed.ts`'s file header).
|
|
335
|
+
test('seeds a filter memo chained off the derived memo', () => {
|
|
336
|
+
const { template } = compileAndGenerate(`
|
|
337
|
+
'use client'
|
|
338
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
339
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
340
|
+
const [searchParams] = createSearchParams()
|
|
341
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
342
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
343
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
344
|
+
}
|
|
345
|
+
`)
|
|
346
|
+
expect(template).toContain(
|
|
347
|
+
"{% set tag = (searchParams.get('tag') if (searchParams.get('tag') is defined and searchParams.get('tag') is not none) else '') %}",
|
|
348
|
+
)
|
|
349
|
+
expect(template).toContain('{% set visible = bf.filter_eval(items,')
|
|
350
|
+
expect(template).toContain("{'tag': tag}")
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// The seed-scope guard used to scan the LOWERED Jinja text for bare-word
|
|
354
|
+
// tokens. That let an outer, unbound `p` (shadowed only inside the
|
|
355
|
+
// callback) slip past as if it were the callback's own bound `p` —
|
|
356
|
+
// emitting a bogus seed line. The guard now walks the parsed SOURCE tree
|
|
357
|
+
// with proper lexical scoping (`freeIdentifiers`, inside
|
|
358
|
+
// `computeSsrSeedPlan`), so this shape seeds nothing and falls back to the
|
|
359
|
+
// static ssr-defaults path.
|
|
360
|
+
test('an outer unbound `p` shadowed only inside the callback does not seed', () => {
|
|
361
|
+
const { template } = compileAndGenerate(`
|
|
362
|
+
'use client'
|
|
363
|
+
import { createMemo } from '@barefootjs/client'
|
|
364
|
+
export function C(props: { items: { ok: boolean }[] }) {
|
|
365
|
+
const visible = createMemo(() => props.items.filter((p) => p.ok) && p)
|
|
366
|
+
return <div>{String(visible())}</div>
|
|
367
|
+
}
|
|
368
|
+
`)
|
|
369
|
+
expect(template).not.toContain('{% set visible')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// An out-of-scope bare `_` reference must not seed either.
|
|
373
|
+
test('an out-of-scope bare `_` reference does not seed', () => {
|
|
374
|
+
const { template } = compileAndGenerate(`
|
|
375
|
+
'use client'
|
|
376
|
+
import { createMemo } from '@barefootjs/client'
|
|
377
|
+
export function C(props: { count: number }) {
|
|
378
|
+
const doubled = createMemo(() => props.count * 2 + _)
|
|
379
|
+
return <div>{doubled()}</div>
|
|
380
|
+
}
|
|
381
|
+
`)
|
|
382
|
+
expect(template).not.toContain('{% set doubled')
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
387
|
+
// conformance layer (workstream C): `filter-nested-callback-predicate` /
|
|
388
|
+
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and
|
|
389
|
+
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
390
|
+
// twin, which must render clean).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JinjaAdapter — Conformance Tests
|
|
3
|
+
*
|
|
4
|
+
* Runs the shared adapter conformance corpus (JSX fixtures, template
|
|
5
|
+
* primitives, marker conformance) against the Jinja2 adapter, rendering
|
|
6
|
+
* each fixture end-to-end through real Jinja2 + the bundled
|
|
7
|
+
* `barefootjs.backend_jinja.JinjaBackend` via `renderJinjaComponent`.
|
|
8
|
+
*
|
|
9
|
+
* The Jinja adapter was ported from the Text::Xslate (Kolon) adapter, so
|
|
10
|
+
* the skip / diagnostic sets below start from xslate's and diverge only
|
|
11
|
+
* where the engine genuinely differs. Every divergence carries a one-line
|
|
12
|
+
* rationale.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { runAdapterConformanceTests } from '@barefootjs/adapter-tests'
|
|
16
|
+
import { JinjaAdapter } from '../adapter'
|
|
17
|
+
import { renderJinjaComponent, PythonNotAvailableError } from '../test-render'
|
|
18
|
+
import { conformancePins } from '../conformance-pins'
|
|
19
|
+
|
|
20
|
+
runAdapterConformanceTests({
|
|
21
|
+
name: 'jinja',
|
|
22
|
+
factory: () => new JinjaAdapter(),
|
|
23
|
+
render: renderJinjaComponent,
|
|
24
|
+
// Per-fixture build-time contracts for shapes the Jinja adapter
|
|
25
|
+
// intentionally refuses to lower. Lives in `../conformance-pins` —
|
|
26
|
+
// mirrors xslate's set (the lowering gates are shared code paths in
|
|
27
|
+
// the ported adapter; BF103/BF104 are structural: cross-template
|
|
28
|
+
// child registration / destructure-loop-param limits that apply
|
|
29
|
+
// identically regardless of target template language).
|
|
30
|
+
expectedDiagnostics: conformancePins,
|
|
31
|
+
// Template-primitive registry: `USER_IMPORT_VIA_CONST` and
|
|
32
|
+
// `NO_DOUBLE_REWRITE_OF_PROPS_OBJECT` now pass (#2069) — a bespoke user
|
|
33
|
+
// import can never be added to the string-keyed registry, but the
|
|
34
|
+
// shared `RelocateEnv.loweringMatchers` acceptance path recognises it
|
|
35
|
+
// via a `LoweringPlugin` the case setup registers around the compile
|
|
36
|
+
// (see `packages/adapter-tests/src/cases/template-primitives.ts`). No
|
|
37
|
+
// skips left, so `skipTemplatePrimitives` is omitted entirely.
|
|
38
|
+
skipMarkerConformance: new Set([
|
|
39
|
+
// Same as Hono / Xslate: `/* @client */` markers on TodoApp's keyed
|
|
40
|
+
// `.map` intentionally elide a slot id from the SSR template that
|
|
41
|
+
// the IR still declares (s6). See hono-adapter.test for the contract.
|
|
42
|
+
'todo-app',
|
|
43
|
+
// Same `/* @client */` keyed-map elision (data-table).
|
|
44
|
+
'data-table',
|
|
45
|
+
]),
|
|
46
|
+
onRenderError: (err, id) => {
|
|
47
|
+
if (err instanceof PythonNotAvailableError) {
|
|
48
|
+
console.log(`Skipping [${id}]: ${err.message}`)
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
},
|
|
53
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ported from `packages/adapter-xslate/src/__tests__/xslate-counter.test.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Only the FIRST (structural "compiles to a template") test is ported here.
|
|
5
|
+
* Xslate's second test renders the compiled template through REAL
|
|
6
|
+
* Text::Xslate via a `renderXslateComponent`-equivalent test-render harness
|
|
7
|
+
* (`Bun.spawn(['perl', ...])`). The Jinja analog of that harness
|
|
8
|
+
* (`renderJinjaComponent`, spawning `python3` against the bundled Python
|
|
9
|
+
* runtime) is `src/test-render.ts` — explicitly workstream C, not written
|
|
10
|
+
* here (see `packages/adapter-jinja/CHANGELOG.md` / the task's workstream
|
|
11
|
+
* split). Once `test-render.ts` lands, a real-render counterpart to this
|
|
12
|
+
* test belongs in the conformance test file, not here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { test, expect } from 'bun:test'
|
|
16
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
17
|
+
import { JinjaAdapter } from '../adapter'
|
|
18
|
+
|
|
19
|
+
const COUNTER_SRC = `"use client"
|
|
20
|
+
import { createSignal } from '@barefootjs/client'
|
|
21
|
+
export function Counter({ initial = 0 }: { initial?: number }) {
|
|
22
|
+
const [count, setCount] = createSignal(initial)
|
|
23
|
+
const doubled = () => count() * 2
|
|
24
|
+
return (
|
|
25
|
+
<div class="counter">
|
|
26
|
+
<p>count: {count()}</p>
|
|
27
|
+
<p>doubled: {doubled()}</p>
|
|
28
|
+
<button onClick={() => setCount(n => n + 1)}>+1</button>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}`
|
|
32
|
+
|
|
33
|
+
test('Counter compiles to a Jinja .jinja template', () => {
|
|
34
|
+
const result = compileJSX(COUNTER_SRC, 'Counter.tsx', {
|
|
35
|
+
adapter: new JinjaAdapter(),
|
|
36
|
+
outputIR: true,
|
|
37
|
+
})
|
|
38
|
+
const errors = result.errors.filter(e => e.severity === 'error')
|
|
39
|
+
expect(errors).toEqual([])
|
|
40
|
+
|
|
41
|
+
const tpl = result.files.find(f => f.type === 'markedTemplate')
|
|
42
|
+
expect(tpl).toBeDefined()
|
|
43
|
+
const content = tpl!.content
|
|
44
|
+
// Script registration as `{% set %}` statements (Jinja's `{% %}` tag never
|
|
45
|
+
// prints, so — unlike Kolon's bare `:` line — no throwaway-bind trick is
|
|
46
|
+
// needed to suppress a leaked return value; see `jinja-adapter.ts`'s
|
|
47
|
+
// `generateScriptRegistrations`).
|
|
48
|
+
expect(content).toContain(`bf.register_script('/static/components/barefoot.js')`)
|
|
49
|
+
expect(content).toContain(`bf.register_script('/static/components/Counter.client.js')`)
|
|
50
|
+
// Hydration markers
|
|
51
|
+
expect(content).toContain(`bf-s="{{ bf.scope_attr() }}"`)
|
|
52
|
+
expect(content).toContain(`{{ bf.hydration_attrs() | safe }}`)
|
|
53
|
+
expect(content).toContain(`{{ bf.props_attr() | safe }}`)
|
|
54
|
+
// Text slots — every text interpolation routes through `bf.string(...)`
|
|
55
|
+
// (divergence 2: Python's default stringification diverges from JS's
|
|
56
|
+
// further than Perl's does, so this port wraps every text-position value,
|
|
57
|
+
// unlike the Kolon port's bare `<: $count :>`).
|
|
58
|
+
expect(content).toContain(`{{ bf.text_start("s0") | safe }}{{ bf.string(count) }}{{ bf.text_end() | safe }}`)
|
|
59
|
+
expect(content).toContain(`{{ bf.text_start("s2") | safe }}{{ bf.string(doubled) }}{{ bf.text_end() | safe }}`)
|
|
60
|
+
// Button stays a plain element (onClick is client-only)
|
|
61
|
+
expect(content).toContain(`+1</button>`)
|
|
62
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queryHref(base, { … })` → `bf.query(...)` lowering for the Jinja adapter
|
|
3
|
+
* (#2042). Ported from
|
|
4
|
+
* `packages/adapter-xslate/src/__tests__/query-href.test.ts`. Parity with
|
|
5
|
+
* the go-template / Xslate lowering: the call + object literal are
|
|
6
|
+
* structured IR, so it lowers directly. The shared `query` runtime helper
|
|
7
|
+
* (`barefootjs.runtime`) includes a pair iff its guard is truthy AND its
|
|
8
|
+
* value is a non-empty string, so a plain `key: v` passes guard `true` and a
|
|
9
|
+
* conditional `key: cond ? v : undefined` passes the lowered condition.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, test, expect } from 'bun:test'
|
|
12
|
+
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
13
|
+
import { JinjaAdapter } from '../adapter/jinja-adapter'
|
|
14
|
+
|
|
15
|
+
function template(src: string): string {
|
|
16
|
+
const a = new JinjaAdapter()
|
|
17
|
+
const r = compileJSX(src.trimStart(), 'T.tsx', { adapter: a, outputIR: true })
|
|
18
|
+
const ir = JSON.parse(r.files.find(f => f.type === 'ir')!.content) as ComponentIR
|
|
19
|
+
return a.generate(ir).template
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('queryHref → bf.query (Jinja, #2042)', () => {
|
|
23
|
+
test('a plain value passes guard true', () => {
|
|
24
|
+
const t = template(`
|
|
25
|
+
'use client'
|
|
26
|
+
import { queryHref } from '@barefootjs/client'
|
|
27
|
+
export function P(props: { base: string; tag: string }) {
|
|
28
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
29
|
+
}
|
|
30
|
+
`)
|
|
31
|
+
expect(t).toContain("bf.query(base, 1, 'tag', tag)")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('a conditional include passes the lowered condition as the guard', () => {
|
|
35
|
+
const t = template(`
|
|
36
|
+
'use client'
|
|
37
|
+
import { queryHref } from '@barefootjs/client'
|
|
38
|
+
export function P(props: { base: string; sort: string; tag: string }) {
|
|
39
|
+
return <a href={queryHref(props.base, { sort: props.sort !== 'date' ? props.sort : undefined, tag: props.tag })}>x</a>
|
|
40
|
+
}
|
|
41
|
+
`)
|
|
42
|
+
// Jinja's `!=` handles string inequality directly (like Kolon's `!=`;
|
|
43
|
+
// unlike Perl's `ne`).
|
|
44
|
+
expect(t).toContain("bf.query(base, (sort != 'date'), 'sort', sort, 1, 'tag', tag)")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// A bare-value guard (`flag ? v : undefined`) is JS *string* truthiness —
|
|
48
|
+
// `'0'` is a truthy string in JS. The lowering must normalise it to a
|
|
49
|
+
// non-empty-string test so SSR matches the client / go (where
|
|
50
|
+
// `lowerUrlGuard` emits `ne <value> ""`). Jinja renders the `!== ''` test
|
|
51
|
+
// as `!= ''` (its string inequality), matching the comparison guard above.
|
|
52
|
+
test('a bare-value guard is normalised to a non-empty-string test', () => {
|
|
53
|
+
const t = template(`
|
|
54
|
+
'use client'
|
|
55
|
+
import { queryHref } from '@barefootjs/client'
|
|
56
|
+
export function P(props: { base: string; flag: string; val: string }) {
|
|
57
|
+
return <a href={queryHref(props.base, { q: props.flag ? props.val : undefined })}>x</a>
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
expect(t).toContain("bf.query(base, (flag != ''), 'q', val)")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// An array value (`{ tag: props.tags }`) lowers to the bare receiver
|
|
64
|
+
// expression; the shared `query` runtime helper detects the list at
|
|
65
|
+
// runtime and appends one pair per non-empty member (#2048). No
|
|
66
|
+
// adapter-side change beyond passing the value through.
|
|
67
|
+
test('an array value passes the receiver expression for the helper to append', () => {
|
|
68
|
+
const t = template(`
|
|
69
|
+
'use client'
|
|
70
|
+
import { queryHref } from '@barefootjs/client'
|
|
71
|
+
export function P(props: { base: string; tags: string[] }) {
|
|
72
|
+
return <a href={queryHref(props.base, { tag: props.tags })}>x</a>
|
|
73
|
+
}
|
|
74
|
+
`)
|
|
75
|
+
expect(t).toContain("bf.query(base, 1, 'tag', tags)")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('an aliased import is recognised', () => {
|
|
79
|
+
const t = template(`
|
|
80
|
+
'use client'
|
|
81
|
+
import { queryHref as qh } from '@barefootjs/client'
|
|
82
|
+
export function P(props: { base: string; tag: string }) {
|
|
83
|
+
return <a href={qh(props.base, { tag: props.tag })}>x</a>
|
|
84
|
+
}
|
|
85
|
+
`)
|
|
86
|
+
expect(t).toContain("bf.query(base, 1, 'tag', tag)")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('a dynamic (non-literal) params object falls back (no bf.query)', () => {
|
|
90
|
+
const t = template(`
|
|
91
|
+
'use client'
|
|
92
|
+
import { queryHref } from '@barefootjs/client'
|
|
93
|
+
export function P(props: { base: string; q: Record<string, string> }) {
|
|
94
|
+
return <a href={queryHref(props.base, props.q)}>x</a>
|
|
95
|
+
}
|
|
96
|
+
`)
|
|
97
|
+
expect(t).not.toContain('.query')
|
|
98
|
+
})
|
|
99
|
+
})
|