@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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JinjaAdapter — conditional-spread / nullish-omission / hyphenated-key
|
|
3
|
+
* regression tests (#textarea / #checkbox Phase 2b parity).
|
|
4
|
+
*
|
|
5
|
+
* Ported from `packages/adapter-xslate/src/__tests__/xslate-spread-attrs.test.ts`,
|
|
6
|
+
* translating each expected template string to Jinja syntax. The shared
|
|
7
|
+
* adapter-conformance fixtures (`textarea`, `checkbox`) only exercise the
|
|
8
|
+
* falsy branch of the conditional spread and the unset branch of the
|
|
9
|
+
* optional attr, so these unit tests pin the truthy / present branches the
|
|
10
|
+
* fixtures can't reach.
|
|
11
|
+
*
|
|
12
|
+
* One test (`hyphenated child attr hash key`) is NOT a byte-for-byte
|
|
13
|
+
* translation: Jinja dict-literal keys are ALWAYS quoted (`jinjaHashKey`
|
|
14
|
+
* unconditionally single-quotes, unlike Kolon's bareword-key sugar —
|
|
15
|
+
* `{key: value}` in Jinja/Python means "look up variable `key`", not the
|
|
16
|
+
* string `"key"` — see `lib/jinja-naming.ts`'s docstring), so there is no
|
|
17
|
+
* "stays unquoted" case to assert for a bare-identifier-safe prop name like
|
|
18
|
+
* `size`; only that BOTH keys are correctly quoted.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { test, expect, describe } from 'bun:test'
|
|
22
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
23
|
+
import type { ComponentIR } from '@barefootjs/jsx'
|
|
24
|
+
import { JinjaAdapter } from '../adapter'
|
|
25
|
+
|
|
26
|
+
function compileToIR(source: string, adapter?: JinjaAdapter): ComponentIR {
|
|
27
|
+
const result = compileJSX(source.trimStart(), 'test.tsx', {
|
|
28
|
+
adapter: adapter ?? new JinjaAdapter(),
|
|
29
|
+
outputIR: true,
|
|
30
|
+
})
|
|
31
|
+
const irFile = result.files.find(f => f.type === 'ir')
|
|
32
|
+
if (!irFile) throw new Error('No IR output')
|
|
33
|
+
return JSON.parse(irFile.content) as ComponentIR
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compileAndGenerate(source: string, adapter?: JinjaAdapter) {
|
|
37
|
+
const a = adapter ?? new JinjaAdapter()
|
|
38
|
+
const ir = compileToIR(source, a)
|
|
39
|
+
return a.generate(ir)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('JinjaAdapter - conditional inline-object spread (textarea aria-describedby)', () => {
|
|
43
|
+
// `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to a Jinja inline
|
|
44
|
+
// ternary of dicts so the falsy `{}` branch OMITS the key
|
|
45
|
+
// (bf.spread_attrs does not emit empty entries). The shared fixture only
|
|
46
|
+
// exercises the falsy branch; this pins the truthy one.
|
|
47
|
+
test('emits a Jinja inline ternary of dicts through bf.spread_attrs', () => {
|
|
48
|
+
const { template } = compileAndGenerate(`
|
|
49
|
+
function Box({ describedBy }: { describedBy?: string }) {
|
|
50
|
+
return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
|
|
51
|
+
}
|
|
52
|
+
`)
|
|
53
|
+
expect(template).toContain(
|
|
54
|
+
"bf.spread_attrs(({'aria-describedby': describedBy} if bf.truthy(describedBy) else {}))",
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('resolves the value reference and preserves the static key for a second prop', () => {
|
|
59
|
+
const { template } = compileAndGenerate(`
|
|
60
|
+
function Box({ label }: { label: string }) {
|
|
61
|
+
return <div {...(label ? { 'data-label': label } : {})} />
|
|
62
|
+
}
|
|
63
|
+
`)
|
|
64
|
+
expect(template).toContain(
|
|
65
|
+
"bf.spread_attrs(({'data-label': label} if bf.truthy(label) else {}))",
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('falls back to BF101 for a computed (non-static) object key', () => {
|
|
70
|
+
const adapter = new JinjaAdapter()
|
|
71
|
+
const ir = compileToIR(`
|
|
72
|
+
function Box({ k, v }: { k?: string; v?: string }) {
|
|
73
|
+
return <div {...(v ? { [k]: v } : {})} />
|
|
74
|
+
}
|
|
75
|
+
`, adapter)
|
|
76
|
+
adapter.generate(ir)
|
|
77
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
78
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('JinjaAdapter - local-const conditional-spread resolution (#checkbox icon)', () => {
|
|
83
|
+
// A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, spread as a bare
|
|
84
|
+
// identifier (`{...attrs}`), resolves through the same Jinja
|
|
85
|
+
// ternary-of-dicts lowering as the inline form. CheckIcon's
|
|
86
|
+
// `const sizeAttrs = size ? {…} : {}` is exactly this shape.
|
|
87
|
+
test('resolves a bare-identifier spread of a function-scope conditional const', () => {
|
|
88
|
+
const { template } = compileAndGenerate(`
|
|
89
|
+
function Box({ flag }: { flag?: boolean }) {
|
|
90
|
+
const attrs = flag ? { 'data-on': 'yes' } : {}
|
|
91
|
+
return <div {...attrs} />
|
|
92
|
+
}
|
|
93
|
+
`)
|
|
94
|
+
expect(template).toContain(
|
|
95
|
+
"bf.spread_attrs(({'data-on': 'yes'} if bf.truthy(flag) else {}))",
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// A const that aliases another bare identifier must NOT be forwarded (loop
|
|
100
|
+
// guard): the resolver bails, so the spread falls through to the standard
|
|
101
|
+
// lowering emitting the bare `attrs` variable.
|
|
102
|
+
test('does not forward a const that aliases another identifier (loop guard)', () => {
|
|
103
|
+
const { template } = compileAndGenerate(`
|
|
104
|
+
function Box({ other }: { other?: object }) {
|
|
105
|
+
const attrs = other
|
|
106
|
+
return <div {...attrs} />
|
|
107
|
+
}
|
|
108
|
+
`)
|
|
109
|
+
expect(template).toContain('bf.spread_attrs(attrs)')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('JinjaAdapter - Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
|
|
114
|
+
// `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed by a
|
|
115
|
+
// prop inside a conditional-spread object value lowers to an inline
|
|
116
|
+
// bracket-indexed Jinja dict `{...}[key]` — the SAME bracket-index syntax
|
|
117
|
+
// JS itself uses (unlike Kolon, which had to steer around Perl's
|
|
118
|
+
// arrow-deref `->{$key}` to the bracket form). This is CheckIcon's
|
|
119
|
+
// `{ width: sizeMap[size], height: sizeMap[size] }` shape.
|
|
120
|
+
test('lowers an indexed module-const map to an inline bracket-indexed dict', () => {
|
|
121
|
+
const { template } = compileAndGenerate(`
|
|
122
|
+
const sizeMap: Record<string, number> = { sm: 16, md: 20, lg: 24, xl: 32 }
|
|
123
|
+
function Box({ size }: { size?: string }) {
|
|
124
|
+
const attrs = size ? { width: sizeMap[size] } : {}
|
|
125
|
+
return <div {...attrs} />
|
|
126
|
+
}
|
|
127
|
+
`)
|
|
128
|
+
expect(template).toContain(
|
|
129
|
+
"{'sm': 16, 'md': 20, 'lg': 24, 'xl': 32}[size]",
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('lowers string-valued record maps too', () => {
|
|
134
|
+
const { template } = compileAndGenerate(`
|
|
135
|
+
const labelMap: Record<string, string> = { a: 'Alpha', b: 'Beta' }
|
|
136
|
+
function Box({ k }: { k?: string }) {
|
|
137
|
+
const attrs = k ? { 'data-label': labelMap[k] } : {}
|
|
138
|
+
return <div {...attrs} />
|
|
139
|
+
}
|
|
140
|
+
`)
|
|
141
|
+
expect(template).toContain("{'a': 'Alpha', 'b': 'Beta'}[k]")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// A non-scalar record value (object) is out of shape: the spread object value
|
|
145
|
+
// can't lower, so the whole spread falls back to BF101.
|
|
146
|
+
test('refuses a non-scalar record value with BF101 (out-of-shape fallback)', () => {
|
|
147
|
+
const adapter = new JinjaAdapter()
|
|
148
|
+
const ir = compileToIR(`
|
|
149
|
+
const sizeMap: Record<string, object> = { sm: { w: 1 } }
|
|
150
|
+
function Box({ size }: { size?: string }) {
|
|
151
|
+
const attrs = size ? { width: sizeMap[size] } : {}
|
|
152
|
+
return <div {...attrs} />
|
|
153
|
+
}
|
|
154
|
+
`, adapter)
|
|
155
|
+
adapter.generate(ir)
|
|
156
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
157
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('JinjaAdapter - props-object inherited-attribute enumeration (#checkbox)', () => {
|
|
162
|
+
// A SolidJS props-object component reads inherited attributes (`props.id`)
|
|
163
|
+
// not enumerated in `propsParams`. The bare optional attribute must be
|
|
164
|
+
// guarded so it's omitted when unset (Hono parity), even though `id`
|
|
165
|
+
// isn't a declared param.
|
|
166
|
+
test('guards a props-object bare optional attr (props.id) with is defined and is not none', () => {
|
|
167
|
+
const { template } = compileAndGenerate(`
|
|
168
|
+
"use client"
|
|
169
|
+
interface P { tone?: string }
|
|
170
|
+
export function Widget(props: P) {
|
|
171
|
+
return <button id={props.id}>x</button>
|
|
172
|
+
}
|
|
173
|
+
`)
|
|
174
|
+
expect(template).toContain('{% if id is defined and id is not none %}')
|
|
175
|
+
expect(template).toContain('id="{{ bf.string(id) }}"')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('JinjaAdapter - hyphenated child attr dict key (#checkbox)', () => {
|
|
180
|
+
// A child component prop whose JSX name isn't a bare identifier
|
|
181
|
+
// (`<CheckIcon data-slot="..."/>`) must be quoted in the `render_child`
|
|
182
|
+
// dict — same as EVERY other key, since Jinja dict-literal keys are
|
|
183
|
+
// ALWAYS quoted (see the file header for why this diverges from the
|
|
184
|
+
// Kolon port's "only quote when non-bareword-safe" assertion).
|
|
185
|
+
test('quotes every child attribute name in render_child, hyphenated or not', () => {
|
|
186
|
+
const { template } = compileAndGenerate(`
|
|
187
|
+
"use client"
|
|
188
|
+
import { Leaf } from './leaf'
|
|
189
|
+
export function Host() {
|
|
190
|
+
return <div><Leaf data-slot="indicator" size="sm" /></div>
|
|
191
|
+
}
|
|
192
|
+
`)
|
|
193
|
+
expect(template).toContain("'data-slot': 'indicator'")
|
|
194
|
+
expect(template).toContain("'size': 'sm'")
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('JinjaAdapter - nullish optional-attribute omission (textarea rows)', () => {
|
|
199
|
+
// A no-destructure-default, nillable-typed prop is `None` when the caller
|
|
200
|
+
// omits it; guard its bare-reference attribute with a Jinja
|
|
201
|
+
// "is defined and is not none" test so it DROPS instead of rendering
|
|
202
|
+
// `attr=""` — matching Hono's nullish-attribute omission. Concrete/defaulted
|
|
203
|
+
// props are never `None` and stay unconditional.
|
|
204
|
+
test('guards a no-default nillable attr with a Jinja defined+not-none check', () => {
|
|
205
|
+
const { template } = compileAndGenerate(`
|
|
206
|
+
function C({ rows }: { rows?: number }) {
|
|
207
|
+
return <textarea rows={rows} />
|
|
208
|
+
}
|
|
209
|
+
`)
|
|
210
|
+
expect(template).toContain('{% if rows is defined and rows is not none %}')
|
|
211
|
+
expect(template).toContain('rows="{{ bf.string(rows) }}"')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('leaves a defaulted attr unconditional (scope did not widen)', () => {
|
|
215
|
+
const { template } = compileAndGenerate(`
|
|
216
|
+
function C({ value = '' }: { value?: string }) {
|
|
217
|
+
return <textarea value={value} />
|
|
218
|
+
}
|
|
219
|
+
`)
|
|
220
|
+
// `value` has a destructure default → never None → unconditional, exactly
|
|
221
|
+
// like Hono's value="".
|
|
222
|
+
expect(template).toContain('value="{{ bf.string(value) }}"')
|
|
223
|
+
expect(template).not.toContain('is not none')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component-tree analysis for the Jinja2 template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `packages/adapter-xslate/src/adapter/analysis/component-tree.ts`.
|
|
5
|
+
* Pure functions over the IR — they read no adapter instance state.
|
|
6
|
+
* `collectImportedLoopChildComponentErrors` returns its diagnostics instead
|
|
7
|
+
* of pushing onto the adapter's error list, so the adapter stays the sole
|
|
8
|
+
* owner of `errors`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ComponentIR,
|
|
13
|
+
IRNode,
|
|
14
|
+
IRComponent,
|
|
15
|
+
IRElement,
|
|
16
|
+
IRFragment,
|
|
17
|
+
IRConditional,
|
|
18
|
+
IRLoop,
|
|
19
|
+
IRIfStatement,
|
|
20
|
+
IRProvider,
|
|
21
|
+
IRAsync,
|
|
22
|
+
CompilerError,
|
|
23
|
+
} from '@barefootjs/jsx'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the component needs the client runtime — it owns reactive state
|
|
27
|
+
* (signals / effects / onMount) or the analyzer flagged it as needing init.
|
|
28
|
+
*/
|
|
29
|
+
export function hasClientInteractivity(ir: ComponentIR): boolean {
|
|
30
|
+
return (
|
|
31
|
+
ir.metadata.signals.length > 0 ||
|
|
32
|
+
ir.metadata.effects.length > 0 ||
|
|
33
|
+
ir.metadata.onMounts.length > 0 ||
|
|
34
|
+
(ir.metadata.clientAnalysis?.needsInit ?? false)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a `BF103` diagnostic for every component reference inside a loop body
|
|
40
|
+
* whose name is imported from a relative-path module. Mirror of the Go /
|
|
41
|
+
* Xslate adapter's check — the Jinja adapter has the same
|
|
42
|
+
* cross-template-registration constraint at request time (each `.jinja`
|
|
43
|
+
* component file must be registered with the shared `jinja2.Environment`
|
|
44
|
+
* loader alongside the parent). Returns the diagnostics so the caller pushes
|
|
45
|
+
* them onto its own error list.
|
|
46
|
+
*/
|
|
47
|
+
export function collectImportedLoopChildComponentErrors(
|
|
48
|
+
ir: ComponentIR,
|
|
49
|
+
componentName: string,
|
|
50
|
+
): CompilerError[] {
|
|
51
|
+
const errors: CompilerError[] = []
|
|
52
|
+
const relativeImports = new Set<string>()
|
|
53
|
+
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
54
|
+
if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
|
|
55
|
+
if (imp.isTypeOnly) continue
|
|
56
|
+
for (const spec of imp.specifiers) {
|
|
57
|
+
relativeImports.add(spec.alias ?? spec.name)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (relativeImports.size === 0) return errors
|
|
61
|
+
|
|
62
|
+
const loc = { file: componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
63
|
+
const visit = (node: IRNode, inLoop: boolean): void => {
|
|
64
|
+
switch (node.type) {
|
|
65
|
+
case 'component': {
|
|
66
|
+
const comp = node as IRComponent
|
|
67
|
+
if (inLoop && relativeImports.has(comp.name)) {
|
|
68
|
+
errors.push({
|
|
69
|
+
code: 'BF103',
|
|
70
|
+
severity: 'error',
|
|
71
|
+
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Jinja adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
72
|
+
loc: comp.loc ?? loc,
|
|
73
|
+
suggestion: {
|
|
74
|
+
message:
|
|
75
|
+
`Options:\n` +
|
|
76
|
+
` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Jinja template alongside the parent at render time.\n` +
|
|
77
|
+
` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
|
|
78
|
+
` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
for (const child of comp.children) visit(child, inLoop)
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
case 'element':
|
|
86
|
+
for (const child of (node as IRElement).children) visit(child, inLoop)
|
|
87
|
+
break
|
|
88
|
+
case 'fragment':
|
|
89
|
+
for (const child of (node as IRFragment).children) visit(child, inLoop)
|
|
90
|
+
break
|
|
91
|
+
case 'conditional': {
|
|
92
|
+
const cond = node as IRConditional
|
|
93
|
+
visit(cond.whenTrue, inLoop)
|
|
94
|
+
if (cond.whenFalse) visit(cond.whenFalse, inLoop)
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
case 'loop':
|
|
98
|
+
for (const child of (node as IRLoop).children) visit(child, true)
|
|
99
|
+
break
|
|
100
|
+
case 'if-statement': {
|
|
101
|
+
const stmt = node as IRIfStatement
|
|
102
|
+
visit(stmt.consequent, inLoop)
|
|
103
|
+
if (stmt.alternate) visit(stmt.alternate, inLoop)
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
case 'provider':
|
|
107
|
+
for (const child of (node as IRProvider).children) visit(child, inLoop)
|
|
108
|
+
break
|
|
109
|
+
case 'async': {
|
|
110
|
+
const a = node as IRAsync
|
|
111
|
+
visit(a.fallback, inLoop)
|
|
112
|
+
for (const child of a.children) visit(child, inLoop)
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
visit(ir.root, false)
|
|
118
|
+
return errors
|
|
119
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural classifier for JS expressions whose result is a boolean value
|
|
3
|
+
* (or unambiguously stringifies to "true"/"false" in JS).
|
|
4
|
+
*
|
|
5
|
+
* Ported from `packages/adapter-xslate/src/adapter/boolean-result.ts`
|
|
6
|
+
* (itself ported from the Mojo adapter's `bf->bool_str` classifier). Used by
|
|
7
|
+
* the Jinja adapter for TWO purposes — one inherited from Xslate, one new:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Attribute/text stringification** (inherited): route a boolean-shaped
|
|
10
|
+
* reactive binding through the runtime `bf.bool_str` helper so the
|
|
11
|
+
* serialised value matches JS `String(boolean)` ("true"/"false"). Python
|
|
12
|
+
* has a real `bool` type (unlike Perl's `1`/`''`), but Python's own
|
|
13
|
+
* `str(True)` == `"True"` (capitalised) — still wrong for HTML output —
|
|
14
|
+
* so the same explicit routing is required.
|
|
15
|
+
* 2. **Condition-position truthy wrapping** (new — `isBooleanResultParsed` is
|
|
16
|
+
* exported, not just the string-based `isBooleanResultExpr`): Python
|
|
17
|
+
* truthiness diverges from JS specifically on empty containers (`[]` /
|
|
18
|
+
* `{}` are JS-truthy, Python-falsy). Perl doesn't have this problem — a
|
|
19
|
+
* Perl array/hash REFERENCE is always true, matching JS objects/arrays
|
|
20
|
+
* being unconditionally truthy — which is why Xslate never needed a
|
|
21
|
+
* truthy-routing layer for `if`/ternary/`&&`/`||` conditions. The Jinja
|
|
22
|
+
* adapter's condition-emission call sites (see `jinja-adapter.ts`'s
|
|
23
|
+
* `convertConditionToJinja`) reuse this SAME structural classifier: a
|
|
24
|
+
* condition that is already unambiguously boolean-shaped emits directly;
|
|
25
|
+
* everything else is wrapped in `bf.truthy(...)` (a JS-faithful
|
|
26
|
+
* `ToBoolean`) before being used as an `{% if %}` / ternary test.
|
|
27
|
+
*
|
|
28
|
+
* The classifier walks a `ParsedExpr` produced by
|
|
29
|
+
* `@barefootjs/jsx::parseExpression` — same AST the filter / loop lowerings
|
|
30
|
+
* already use — so detection is structural rather than regex-text-matching.
|
|
31
|
+
* Wrapped expression text is left to the caller's existing
|
|
32
|
+
* `convertExpressionToJinja` pipeline; this module only decides whether to
|
|
33
|
+
* wrap.
|
|
34
|
+
*
|
|
35
|
+
* Detected shapes:
|
|
36
|
+
* - `binary` with a comparison operator (`<`, `>`, `<=`, `>=`, `==`, `===`,
|
|
37
|
+
* `!=`, `!==`)
|
|
38
|
+
* - `unary` with logical `!`
|
|
39
|
+
* - `literal` with `literalType: 'boolean'`
|
|
40
|
+
* - `logical` (`&&` / `||` / `??`) when both sides are themselves
|
|
41
|
+
* boolean-result (catches `x > 0 && y < 10`; intentionally does NOT
|
|
42
|
+
* catch `x() || 'fallback'` whose right side stringifies as a regular
|
|
43
|
+
* value)
|
|
44
|
+
* - `conditional` (`?:`) when both branches are themselves boolean-result
|
|
45
|
+
*
|
|
46
|
+
* Anything else returns `false` — including bare identifiers (`accepted`)
|
|
47
|
+
* and call expressions (`accepted()`) whose return type the adapter has no
|
|
48
|
+
* way to infer from source text alone.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { parseExpression, type ParsedExpr } from '@barefootjs/jsx'
|
|
52
|
+
|
|
53
|
+
const COMPARISON_OPS = new Set([
|
|
54
|
+
'<',
|
|
55
|
+
'>',
|
|
56
|
+
'<=',
|
|
57
|
+
'>=',
|
|
58
|
+
'==',
|
|
59
|
+
'===',
|
|
60
|
+
'!=',
|
|
61
|
+
'!==',
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structural boolean-result check over an already-parsed `ParsedExpr` tree.
|
|
66
|
+
* Exported (unlike Xslate's private equivalent) so the condition-position
|
|
67
|
+
* truthy-wrapping call sites can reuse it without a stringify → re-parse
|
|
68
|
+
* round-trip.
|
|
69
|
+
*/
|
|
70
|
+
export function isBooleanResultParsed(node: ParsedExpr): boolean {
|
|
71
|
+
switch (node.kind) {
|
|
72
|
+
case 'literal':
|
|
73
|
+
return node.literalType === 'boolean'
|
|
74
|
+
case 'binary':
|
|
75
|
+
return COMPARISON_OPS.has(node.op)
|
|
76
|
+
case 'unary':
|
|
77
|
+
return node.op === '!'
|
|
78
|
+
case 'logical':
|
|
79
|
+
// `x > 0 && y < 10` is boolean; `x() || 'fallback'` is not.
|
|
80
|
+
// Only both-sides-boolean qualifies.
|
|
81
|
+
return (
|
|
82
|
+
isBooleanResultParsed(node.left) && isBooleanResultParsed(node.right)
|
|
83
|
+
)
|
|
84
|
+
case 'conditional':
|
|
85
|
+
// `cond ? bool : bool` is boolean; `cond ? 'a' : 'b'` is not.
|
|
86
|
+
return (
|
|
87
|
+
isBooleanResultParsed(node.consequent) &&
|
|
88
|
+
isBooleanResultParsed(node.alternate)
|
|
89
|
+
)
|
|
90
|
+
default:
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isBooleanResultExpr(expr: string): boolean {
|
|
96
|
+
const parsed = parseExpression(expr.trim())
|
|
97
|
+
if (!parsed) return false
|
|
98
|
+
return isBooleanResultParsed(parsed)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* True when `expr`'s top-level shape is an explicit JS `String(x)` call
|
|
103
|
+
* (the `EVAL_BUILTIN_IDENTS` builtin the compiler recognizes structurally —
|
|
104
|
+
* `packages/jsx/src/expression-parser.ts`'s `EVAL_BUILTIN_IDENTS`; lowered
|
|
105
|
+
* by this adapter's `String` template primitive to `bf.string(x)`, see
|
|
106
|
+
* `lib/constants.ts`).
|
|
107
|
+
*
|
|
108
|
+
* Guards the `isAriaBooleanAttr`-driven `bf.bool_str(...)` override in
|
|
109
|
+
* `jinja-adapter.ts`'s `elementAttrEmitter`: `bf.string` and `bf.bool_str`
|
|
110
|
+
* produce IDENTICAL text for a real Python `bool` (both are `"true"` /
|
|
111
|
+
* `"false"`), so applying `bf.bool_str` to `String(x)`'s ALREADY-STRINGIFIED
|
|
112
|
+
* result is not a no-op — it is a Python-truthiness test over that STRING
|
|
113
|
+
* ("false" is a non-empty Python string, hence truthy, so
|
|
114
|
+
* `bf.bool_str(bf.string(false))` would wrongly render `"true"`). The Kolon
|
|
115
|
+
* port has the identical double-wrap shape and "works" only by an
|
|
116
|
+
* unrelated accident (`JSON::PP::Boolean` stringifies to `"0"`/`"1"`, and
|
|
117
|
+
* Perl specifically treats the STRING `"0"` as falsy) that doesn't hold in
|
|
118
|
+
* Python. An author who explicitly writes `String(...)` has already opted
|
|
119
|
+
* into JS `String()` semantics — `bf.string(x)` alone (which DOES special-
|
|
120
|
+
* case booleans, see `runtime.js_string`) is the complete, correct
|
|
121
|
+
* lowering; no attribute-name-driven override should run again on top of
|
|
122
|
+
* it.
|
|
123
|
+
*/
|
|
124
|
+
export function isExplicitStringCall(expr: string): boolean {
|
|
125
|
+
const parsed = parseExpression(expr.trim())
|
|
126
|
+
return (
|
|
127
|
+
!!parsed &&
|
|
128
|
+
parsed.kind === 'call' &&
|
|
129
|
+
parsed.callee.kind === 'identifier' &&
|
|
130
|
+
parsed.callee.name === 'String' &&
|
|
131
|
+
parsed.args.length === 1
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* ARIA attributes whose spec values are `"true"`, `"false"`, and (for
|
|
137
|
+
* tri-state members) `"mixed"`. When a fixture binds one of these to an
|
|
138
|
+
* arbitrary JS expression (`aria-checked={accepted()}`), the expression's
|
|
139
|
+
* actual type isn't recoverable from source text — but the attribute name
|
|
140
|
+
* itself witnesses that the binding is boolean-shaped. Routing these through
|
|
141
|
+
* `bf.bool_str` produces the spec-canonical `"true"` / `"false"` even when
|
|
142
|
+
* the expression is opaque.
|
|
143
|
+
*
|
|
144
|
+
* Deliberately conservative — only includes ARIA attributes whose spec value
|
|
145
|
+
* set is exactly `true | false` or `true | false | mixed`. Tokenised ARIA
|
|
146
|
+
* attributes (`aria-current` is `page | step | …`, `aria-sort` is
|
|
147
|
+
* `ascending | descending | …`) are intentionally excluded so a
|
|
148
|
+
* string-valued binding doesn't get coerced to `"true"` / `"false"`.
|
|
149
|
+
*/
|
|
150
|
+
const ARIA_BOOLEAN_ATTRS = new Set([
|
|
151
|
+
// Strict boolean state (true | false; some allow `undefined` = attribute
|
|
152
|
+
// absent, which the runtime emits as no-attr regardless).
|
|
153
|
+
'aria-atomic',
|
|
154
|
+
'aria-busy',
|
|
155
|
+
'aria-disabled',
|
|
156
|
+
'aria-hidden',
|
|
157
|
+
'aria-modal',
|
|
158
|
+
'aria-multiline',
|
|
159
|
+
'aria-multiselectable',
|
|
160
|
+
'aria-readonly',
|
|
161
|
+
'aria-required',
|
|
162
|
+
// true | false | undefined (absent) — selection / disclosure state.
|
|
163
|
+
'aria-selected',
|
|
164
|
+
'aria-expanded',
|
|
165
|
+
// Tri-state (true | false | mixed). The `bool_str` helper only maps
|
|
166
|
+
// truthy / falsy to true / false — a fixture that wants the literal
|
|
167
|
+
// "mixed" would bind a string-valued JSX attr (`aria-checked="mixed"`),
|
|
168
|
+
// which lowers through the `literal` emit path and never touches this
|
|
169
|
+
// code.
|
|
170
|
+
'aria-checked',
|
|
171
|
+
'aria-pressed',
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
export function isAriaBooleanAttr(name: string): boolean {
|
|
175
|
+
return ARIA_BOOLEAN_ATTRS.has(name)
|
|
176
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract the extracted expression-emitter modules depend on instead of
|
|
3
|
+
* the concrete `JinjaAdapter`.
|
|
4
|
+
*
|
|
5
|
+
* Ported from `packages/adapter-xslate/src/adapter/emit-context.ts`. The
|
|
6
|
+
* Jinja adapter's top-level expression lowering is mutually recursive with
|
|
7
|
+
* the adapter's own const/record resolution and its filter-predicate
|
|
8
|
+
* emitter, so the extracted `JinjaTopLevelEmitter` still needs to call back
|
|
9
|
+
* into shared per-compile state and recursive entry points.
|
|
10
|
+
* `JinjaEmitContext` is that seam: the emitter takes a `JinjaEmitContext`
|
|
11
|
+
* built by the adapter's private `emitCtx` getter (the adapter does NOT
|
|
12
|
+
* `implements` this interface, so the wrapped members stay private and off
|
|
13
|
+
* its exported public type). The emitter depends on this narrow interface
|
|
14
|
+
* rather than the full class, so the coupling is explicit and it's
|
|
15
|
+
* unit-testable against a stub.
|
|
16
|
+
*
|
|
17
|
+
* Keep this surface minimal: add a member only when an extracted module
|
|
18
|
+
* genuinely needs it, so the seam documents the real cross-module coupling
|
|
19
|
+
* rather than re-exposing the whole adapter.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
|
|
23
|
+
|
|
24
|
+
export interface JinjaEmitContext {
|
|
25
|
+
/**
|
|
26
|
+
* (#1922) Local binding names the request-scoped `searchParams()` env signal
|
|
27
|
+
* is imported under. Non-empty enables the env-signal method-call lowering.
|
|
28
|
+
*/
|
|
29
|
+
readonly _searchParamsLocals: Set<string>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inline a module-scope pure string-literal const by name as the resolved
|
|
33
|
+
* literal value, or null when the name is not such a const.
|
|
34
|
+
*/
|
|
35
|
+
_resolveModuleStringConst(name: string): string | null
|
|
36
|
+
|
|
37
|
+
/** Resolve a literal const (`const totalPages = 5`) to its Jinja value, or null. */
|
|
38
|
+
_resolveLiteralConst(name: string): string | null
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a static property access on a module object-literal const
|
|
42
|
+
* (`variantClasses.ghost`) to its Jinja value at compile time, or null.
|
|
43
|
+
*/
|
|
44
|
+
_resolveStaticRecordLiteral(objectName: string, key: string): string | null
|
|
45
|
+
|
|
46
|
+
/** Record a BF101 unsupported-expression diagnostic. */
|
|
47
|
+
_recordExprBF101(message: string, reason?: string): void
|
|
48
|
+
|
|
49
|
+
/** Lower a filter/predicate body to its Jinja form, bound to `param`. */
|
|
50
|
+
_renderJinjaFilterExprPublic(expr: ParsedExpr, param: string): string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The contract the extracted object-literal / conditional-spread lowering
|
|
55
|
+
* (`spread/spread-codegen.ts`) depends on. Declared separately from
|
|
56
|
+
* `JinjaEmitContext` so each extracted module's real coupling is documented
|
|
57
|
+
* precisely. Mirror of the Xslate adapter's `XslateSpreadContext`.
|
|
58
|
+
*/
|
|
59
|
+
export interface JinjaSpreadContext {
|
|
60
|
+
/** Component name, for diagnostic source locations. */
|
|
61
|
+
readonly componentName: string
|
|
62
|
+
|
|
63
|
+
/** Per-compile diagnostic list the spread lowering appends to. */
|
|
64
|
+
readonly errors: CompilerError[]
|
|
65
|
+
|
|
66
|
+
/** Local-constant metadata, for resolving `Record[key]` spread values. */
|
|
67
|
+
readonly localConstants: IRMetadata['localConstants']
|
|
68
|
+
|
|
69
|
+
/** Prop params, for classifying a bare-identifier index as a prop. */
|
|
70
|
+
readonly propsParams: { name: string }[]
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Lower a JS expression to its Jinja form (the core recursive entry).
|
|
74
|
+
*
|
|
75
|
+
* When the IR already carries a structured `ParsedExpr` tree, pass it as
|
|
76
|
+
* `preParsed` so the converter threads it straight through instead of
|
|
77
|
+
* re-parsing `expr`. With `preParsed` set, `expr` is unused for parsing
|
|
78
|
+
* (the converter derives any diagnostic text from the tree), so callers
|
|
79
|
+
* may pass `''`.
|
|
80
|
+
*/
|
|
81
|
+
convertExpressionToJinja(expr: string, preParsed?: ParsedExpr): string
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Lower a JS expression to a Jinja CONDITION (routes through `bf.truthy`
|
|
85
|
+
* unless the expression is structurally already boolean-shaped — see
|
|
86
|
+
* `boolean-result.ts`). Used for the conditional-spread ternary's test,
|
|
87
|
+
* which is a condition position, not a value position. Same `preParsed`
|
|
88
|
+
* contract as `convertExpressionToJinja`.
|
|
89
|
+
*/
|
|
90
|
+
convertConditionToJinja(expr: string, preParsed?: ParsedExpr): string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The contract the extracted in-template memo / context seeding
|
|
95
|
+
* (`memo/seed.ts`) depends on. The seed lowering recurses into the core
|
|
96
|
+
* expression lowering to compute a derived signal/memo value or a context
|
|
97
|
+
* default; that recursive entry is its only adapter coupling.
|
|
98
|
+
*/
|
|
99
|
+
export interface JinjaMemoContext {
|
|
100
|
+
/**
|
|
101
|
+
* Lower a JS expression to its Jinja form (the core recursive entry). See
|
|
102
|
+
* `JinjaSpreadContext.convertExpressionToJinja` for the `preParsed` contract.
|
|
103
|
+
*/
|
|
104
|
+
convertExpressionToJinja(expr: string, preParsed?: ParsedExpr): string
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Per-compile diagnostic list `convertExpressionToJinja` appends to on an
|
|
108
|
+
* unsupported shape (`_recordExprBF101`). `memo/seed.ts`'s
|
|
109
|
+
* `generateDerivedMemoSeed` is a SPECULATIVE "try this in-template
|
|
110
|
+
* recomputation, else fall back to the static ssrDefault seed" attempt per
|
|
111
|
+
* plan step — unlike every other `convertExpressionToJinja` call site, a
|
|
112
|
+
* failure here must NOT become a hard compile error, so it snapshots this
|
|
113
|
+
* array's length before calling in and truncates back to it on failure
|
|
114
|
+
* (discarding whatever `_recordExprBF101` appended) rather than letting
|
|
115
|
+
* the error escape.
|
|
116
|
+
*/
|
|
117
|
+
readonly errors: CompilerError[]
|
|
118
|
+
}
|