@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.
Files changed (88) hide show
  1. package/README.md +66 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/boolean-result.d.ts +84 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +106 -0
  7. package/dist/adapter/emit-context.d.ts.map +1 -0
  8. package/dist/adapter/expr/array-method.d.ts +75 -0
  9. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  10. package/dist/adapter/expr/emitters.d.ts +143 -0
  11. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  12. package/dist/adapter/expr/operand.d.ts +25 -0
  13. package/dist/adapter/expr/operand.d.ts.map +1 -0
  14. package/dist/adapter/index.d.ts +6 -0
  15. package/dist/adapter/index.d.ts.map +1 -0
  16. package/dist/adapter/index.js +189097 -0
  17. package/dist/adapter/jinja-adapter.d.ts +394 -0
  18. package/dist/adapter/jinja-adapter.d.ts.map +1 -0
  19. package/dist/adapter/lib/constants.d.ts +21 -0
  20. package/dist/adapter/lib/constants.d.ts.map +1 -0
  21. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  22. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  23. package/dist/adapter/lib/jinja-naming.d.ts +77 -0
  24. package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
  25. package/dist/adapter/lib/types.d.ts +27 -0
  26. package/dist/adapter/lib/types.d.ts.map +1 -0
  27. package/dist/adapter/memo/seed.d.ts +81 -0
  28. package/dist/adapter/memo/seed.d.ts.map +1 -0
  29. package/dist/adapter/props/prop-classes.d.ts +33 -0
  30. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  31. package/dist/adapter/spread/spread-codegen.d.ts +61 -0
  32. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  33. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  34. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  35. package/dist/build.d.ts +28 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +189117 -0
  38. package/dist/conformance-pins.d.ts +12 -0
  39. package/dist/conformance-pins.d.ts.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +189118 -0
  43. package/package.json +66 -0
  44. package/python/VERSION +1 -0
  45. package/python/barefootjs/__init__.py +57 -0
  46. package/python/barefootjs/backend_jinja.py +125 -0
  47. package/python/barefootjs/evaluator.py +787 -0
  48. package/python/barefootjs/runtime.py +1334 -0
  49. package/python/barefootjs/search_params.py +89 -0
  50. package/python/pyproject.toml +32 -0
  51. package/python/tests/__init__.py +0 -0
  52. package/python/tests/test_eval_vectors.py +88 -0
  53. package/python/tests/test_evaluator.py +406 -0
  54. package/python/tests/test_helper_vectors.py +288 -0
  55. package/python/tests/test_omit.py +62 -0
  56. package/python/tests/test_props_attr.py +54 -0
  57. package/python/tests/test_query.py +41 -0
  58. package/python/tests/test_render.py +102 -0
  59. package/python/tests/test_render_child.py +96 -0
  60. package/python/tests/test_search_params.py +50 -0
  61. package/python/tests/test_spread_attrs.py +86 -0
  62. package/python/tests/test_template_primitives.py +347 -0
  63. package/python/tests/vector-divergences.json +42 -0
  64. package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
  65. package/src/__tests__/jinja-adapter.test.ts +53 -0
  66. package/src/__tests__/jinja-counter.test.ts +62 -0
  67. package/src/__tests__/jinja-query-href.test.ts +99 -0
  68. package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +176 -0
  71. package/src/adapter/emit-context.ts +118 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/expr/operand.ts +35 -0
  75. package/src/adapter/index.ts +6 -0
  76. package/src/adapter/jinja-adapter.ts +1747 -0
  77. package/src/adapter/lib/constants.ts +33 -0
  78. package/src/adapter/lib/ir-scope.ts +95 -0
  79. package/src/adapter/lib/jinja-naming.ts +114 -0
  80. package/src/adapter/lib/types.ts +30 -0
  81. package/src/adapter/memo/seed.ts +132 -0
  82. package/src/adapter/props/prop-classes.ts +65 -0
  83. package/src/adapter/spread/spread-codegen.ts +166 -0
  84. package/src/adapter/value/parsed-literal.ts +76 -0
  85. package/src/build.ts +37 -0
  86. package/src/conformance-pins.ts +101 -0
  87. package/src/index.ts +9 -0
  88. 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
+ })