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