/** * Ported from the Xslate-specific describes in * `packages/adapter-xslate/src/__tests__/xslate-adapter.test.ts` (lines * 152-400): SSR context propagation, memo seeding, signal seeding + * data-key, #1966 @client attribute deferral, #2073 map_eval, #2018 P2 * predicate lowering, #2075 searchParams()-derived memo seeding. Expected * template strings are translated to Jinja syntax. `runAdapterConformanceTests` * itself (lines 24-150) and `src/test-render.ts` are workstream C and are NOT * ported here. * * One describe (`prop-derived signal SSR seeding + data-key`) diverges * behaviourally, not just syntactically: the "does NOT in-template-seed a * same-name signal" Xslate test is inverted here to "DOES seed" — see * `memo/seed.ts`'s file header for why Jinja's `{% set %}` doesn't have * Kolon's `my`-shadowing hazard. * * The "#2075 searchParams()-derived memo seeding" describe also diverges: the * "filter memo chained off the derived memo" case asserts a `bf.filter_eval(` * seed (not Xslate's `$bf.filter(` lambda-closure), and asserts the * evaluator's captured env dict (`{'tag': tag}`) rather than a bare `$tag` * lambda-body reference — see `memo/seed.ts`'s file header on * `materializeGetterCalls` (divergence 3: Jinja has no lambda fallback, so a * sibling-getter call inside a predicate must materialize to a free var the * evaluator can capture, instead of closing over the lexical directly). */ import { describe, test, expect } from 'bun:test' import { JinjaAdapter } from '../adapter' import { compileJSX, type ComponentIR } from '@barefootjs/jsx' // ============================================================================= // Helpers // ============================================================================= function compileToIR(source: string): ComponentIR { const result = compileJSX(source.trimStart(), 'test.tsx', { adapter: new JinjaAdapter(), outputIR: true, }) const irFile = result.files.find(f => f.type === 'ir') if (!irFile) throw new Error('No IR output') return JSON.parse(irFile.content) as ComponentIR } function compileAndGenerate(source: string) { return new JinjaAdapter().generate(compileToIR(source)) } // ============================================================================= // Jinja-Specific Tests // ============================================================================= describe('JinjaAdapter - SSR context propagation (#1297)', () => { // `` brackets its children with inline provide/revoke // calls (both return '' so the `{{ … }}` interpolation discards them); // descendant `useContext` consumers read the value during the same // render. test('provider brackets children with provide_context / revoke_context', () => { const { template } = compileAndGenerate(` 'use client' import { createContext, useContext } from '@barefootjs/client' const ThemeContext = createContext('light') export function ThemeRoot() { return
} function ThemeLabel() { const theme = useContext(ThemeContext); return {theme} } `) expect(template).toContain("bf.provide_context('ThemeContext', 'dark')") expect(template).toContain("bf.revoke_context('ThemeContext')") expect(template.indexOf('provide_context')).toBeLessThan(template.indexOf('render_child')) expect(template.indexOf('render_child')).toBeLessThan(template.indexOf('revoke_context')) }) test('consumer seeds its local from use_context with the createContext default', () => { const { template } = compileAndGenerate(` 'use client' import { createContext, useContext } from '@barefootjs/client' const ThemeContext = createContext('light') export function ThemeLabel() { const theme = useContext(ThemeContext); return {theme} } `) expect(template).toContain("{% set theme = bf.use_context('ThemeContext', 'light') %}") }) }) describe('JinjaAdapter - prop-derived memo SSR seeding (#1297)', () => { // A memo whose body can't be statically folded (`props.value * 10`) gets a // `null` SSR default; the adapter computes it in-template from the seeded // prop var so the child renders the value instead of empty. test('seeds a prop-derived memo from the prop var', () => { const { template } = compileAndGenerate(` 'use client' import { createMemo } from '@barefootjs/client' export function Child(props: { value: number }) { const displayValue = createMemo(() => props.value * 10) return {displayValue()} } `) expect(template).toContain('{% set displayValue = value * 10 %}') }) test('seeds a memo over a destructured prop', () => { const { template } = compileAndGenerate(` 'use client' import { createMemo } from '@barefootjs/client' export function Child({ value }: { value: number }) { const displayValue = createMemo(() => value * 10) return {displayValue()} } `) expect(template).toContain('{% set displayValue = value * 10 %}') }) }) describe('JinjaAdapter - prop-derived signal SSR seeding + data-key (#1297, toggle-shared)', () => { test('seeds a prop-derived (different-name) signal from the prop var', () => { const { template } = compileAndGenerate(` 'use client' import { createSignal } from '@barefootjs/client' export function Item(props: { defaultOn?: boolean }) { const [on, setOn] = createSignal(props.defaultOn ?? false) return } `) expect(template).toContain('{% set on = (defaultOn if (defaultOn is defined and defaultOn is not none) else false) %}') }) // Divergence from the Kolon port: Jinja's `{% set x = x + 1 %}` safely // resolves the right-hand `x` from the enclosing scope (no `my`-shadowing // hazard), so a same-name signal IS seeded in-template here — strictly // more correct than leaving it on the static default. See // `memo/seed.ts`'s file header. test('DOES in-template-seed a same-name signal (no Kolon `my`-shadow hazard in Jinja)', () => { const { template } = compileAndGenerate(` 'use client' import { createSignal } from '@barefootjs/client' export function C(props: { x?: number }) { const [x, setX] = createSignal(props.x ?? 7) return {x()} } `) expect(template).toContain('{% set x = (x if (x is defined and x is not none) else 7) %}') }) test('emits data_key_attr on the component root', () => { const { template } = compileAndGenerate(` export function Item() { return
hi
} `) expect(template).toContain('bf.data_key_attr()') }) test('emits data_key_attr on each branch root of an if-statement root', () => { const { template } = compileAndGenerate(` export function Item({ on }: { on?: boolean }) { if (on) return
A
return
B
} `) const count = (template.match(/bf\.data_key_attr\(\)/g) ?? []).length expect(count).toBe(2) }) }) // ============================================================================= // #1966 — `/* @client */` defers ATTRIBUTE bindings (not just child/text) // ============================================================================= // // `renderAttributes` skips SSR emission for `attr.clientOnly`, so a // deferred attribute predicate is omitted from the Jinja template (and the // unsupported-expression lowering is never reached → no BF101/BF102). The // client runtime sets the attribute on hydrate. Mirrors the Go / Xslate pins. describe('JinjaAdapter - #1966 @client defers attribute bindings', () => { function compileAttr(attrExpr: string) { const adapter = new JinjaAdapter() const ir = compileToIR(` "use client" import { createSignal } from "@barefootjs/client" export function C() { const [sel] = createSignal(0) const pred = (n: number) => sel() === n return
hi
} `) const template = adapter.generate(ir).template ?? '' const errors = (adapter as unknown as { errors: { code: string }[] }).errors ?? [] return { errors, template } } test('bare emits data-x; @client omits it from SSR', () => { expect(compileAttr('pred(1)').template).toContain('data-x') const deferred = compileAttr('/* @client */ pred(1)') expect(deferred.errors).toEqual([]) expect(deferred.template).not.toContain('data-x') }) }) // #2018 P2: higher-order predicates lower through the runtime evaluator // (`bf.*_eval`), isomorphic with the Go / Xslate `*_eval` helpers. Jinja has // no lambda fallback (divergence 3 in `jinja-adapter.ts`'s header) — a // predicate the evaluator can't model surfaces BF101 instead. describe('JinjaAdapter - #2073 value-producing .map(cb)', () => { // The blog-showcase shape (#1938/#1939): a value-returning `.map` (string // projection, not JSX) lowers through the evaluator — `bf.map_eval` // projects each element (no flatten) and composes through `bf.join`. test('.map(t => `#${t}`).join(" ") emits bf.map_eval composed into bf.join', () => { const { template } = compileAndGenerate(` function TagLine({ tags }: { tags: string[] }) { return

{tags.map((t) => \`#\${t}\`).join(' ')}

} export { TagLine } `) expect(template).toContain("bf.join(bf.map_eval(tags,") expect(template).toContain('"kind":"template-literal"') }) test('.map(u => u.name) emits bf.map_eval with the field projection', () => { const { template } = compileAndGenerate(` function NameList({ users }: { users: { name: string }[] }) { return
{users.map((u) => u.name).join(', ')}
} export { NameList } `) expect(template).toContain('bf.map_eval(users,') expect(template).toContain('"property":"name"') }) }) describe('JinjaAdapter - higher-order predicate lowering (#2018 P2)', () => { test('a serializable predicate lowers to bf.filter_eval with the JSON body + env', () => { // A standalone `.filter().length` exercises the higher-order emitter (the // `.filter().map()` form is a loop-hoist with an inline `{% if %}`, // handled by renderLoop, not this emitter). const { template } = compileAndGenerate(` function A({ items }: { items: { done: boolean }[] }) { return
{items.filter(x => x.done).length}
} export { A } `) expect(template).toContain('bf.filter_eval(') expect(template).toContain('"property":"done"') expect(template).toContain("'x'") }) test('.find / .findLast share bf.find_eval, distinguished by the forward flag', () => { const find = compileAndGenerate(` function A({ items }: { items: { done: boolean }[] }) { return
{items.find(x => x.done) ? 'y' : 'n'}
} export { A } `).template expect(find).toContain('bf.find_eval(') // `forward` is a genuine Python bool literal here (`true`/`false`), // unlike the Kolon port's Perl `1`/`0` — see `expr/array-method.ts`. expect(find).toContain(", true, {})") const findLast = compileAndGenerate(` function A({ items }: { items: { done: boolean }[] }) { return
{items.findLast(x => x.done) ? 'y' : 'n'}
} export { A } `).template expect(findLast).toContain('bf.find_eval(') expect(findLast).toContain(", false, {})") }) // #2075: `.includes(x)` joined the evaluator's `array-method` surface // (shared with the Perl/Go evaluator runtimes), so a predicate built from // it now routes through `bf.every_eval` like any other pure predicate — // no BF101, despite being a method-call predicate. test('.includes() in a predicate now lowers via the evaluator, not a BF101 refusal', () => { const { template } = compileAndGenerate(` function A({ items }: { items: { name: string }[] }) { return
{items.every(x => x.name.includes('a')) ? 'y' : 'n'}
} export { A } `) expect(template).toContain('bf.every_eval(') expect(template).toContain('"method":"includes"') expect(template).not.toContain('bf.truthy(\'\')') }) // Divergence 3 (`jinja-adapter.ts`'s header): Jinja has no lambda // expression, so a predicate the evaluator can't serialize (a method call // outside the evaluator's `array-method` gate — only `includes` is // recognized there) has NO fallback — unlike Kolon, which falls back to a // `-> $x { … }` lambda passed to `$bf.every`. This surfaces BF101 instead. test('a method-call predicate outside the evaluator surface has no lambda fallback — surfaces BF101', () => { const adapter = new JinjaAdapter() const ir = compileToIR(` function A({ items }: { items: { name: string }[] }) { return
{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}
} export { A } `) const { template } = adapter.generate(ir) const errors = (adapter as unknown as { errors: { code: string; message: string }[] }).errors expect(errors.some(e => e.code === 'BF101' && e.message.includes("'.every(...)'"))).toBe(true) // No evaluator helper, and — critically — no lambda syntax either; // the condition falls back to the safe `bf.truthy('')` empty-string // sentinel (see `convertExpressionToJinja`'s BF101 path). expect(template).not.toContain('every_eval') expect(template).not.toContain('bf.every(') expect(template).toContain("bf.truthy('')") }) }) // #2075: derived signal/memo seeding now consumes the shared // `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts) instead of // re-deriving scope/support locally — same plan Xslate/Mojo consume, ported // to `{% set %}` syntax. Mirrors // `packages/adapter-xslate/src/__tests__/xslate-adapter.test.ts`'s // "#2075 searchParams()-derived memo seeding" describe. describe('JinjaAdapter - #2075 searchParams()-derived memo seeding', () => { test('seeds an aliased scalar derived memo from the canonical reader', () => { const { template } = compileAndGenerate(` 'use client' import { createMemo, createSearchParams } from '@barefootjs/client' export function SortStatus() { const [sp] = createSearchParams() const sort = createMemo(() => sp().get('sort') ?? 'date') return

sort: {sort()}

} `) expect(template).toContain( "{% set sort = (searchParams.get('sort') if (searchParams.get('sort') is defined and searchParams.get('sort') is not none) else 'date') %}", ) }) // The evaluator-JSON `param` and the `bf` runtime object are // lowering-internal, not out-of-scope template vars (#2075). Jinja routes // the filter predicate through `bf.filter_eval` (divergence 3 — no lambda // fallback), materializing the sibling getter call `tag()` into a bare // free-var read (`materializeGetterCalls`) so the evaluator can serialize // it at all; the captured env then resolves against the `tag` seed line // emitted just above (see `memo/seed.ts`'s file header). test('seeds a filter memo chained off the derived memo', () => { const { template } = compileAndGenerate(` 'use client' import { createMemo, createSearchParams } from '@barefootjs/client' export function TaggedList(props: { items: { title: string; tags: string[] }[] }) { const [searchParams] = createSearchParams() const tag = createMemo(() => searchParams().get('tag') ?? '') const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag()))) return } `) expect(template).toContain( "{% set tag = (searchParams.get('tag') if (searchParams.get('tag') is defined and searchParams.get('tag') is not none) else '') %}", ) expect(template).toContain('{% set visible = bf.filter_eval(items,') expect(template).toContain("{'tag': tag}") }) // The seed-scope guard used to scan the LOWERED Jinja text for bare-word // tokens. That let an outer, unbound `p` (shadowed only inside the // callback) slip past as if it were the callback's own bound `p` — // emitting a bogus seed line. The guard now walks the parsed SOURCE tree // with proper lexical scoping (`freeIdentifiers`, inside // `computeSsrSeedPlan`), so this shape seeds nothing and falls back to the // static ssr-defaults path. test('an outer unbound `p` shadowed only inside the callback does not seed', () => { const { template } = compileAndGenerate(` 'use client' import { createMemo } from '@barefootjs/client' export function C(props: { items: { ok: boolean }[] }) { const visible = createMemo(() => props.items.filter((p) => p.ok) && p) return
{String(visible())}
} `) expect(template).not.toContain('{% set visible') }) // An out-of-scope bare `_` reference must not seed either. test('an out-of-scope bare `_` reference does not seed', () => { const { template } = compileAndGenerate(` 'use client' import { createMemo } from '@barefootjs/client' export function C(props: { count: number }) { const doubled = createMemo(() => props.count * 2 + _) return
{doubled()}
} `) expect(template).not.toContain('{% set doubled') }) }) describe('JinjaAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => { // A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX // attribute name) must not leak into the `{% set %}` capture variable's // identifier — Jinja variable names can't contain `-`. The capture // identifier is purely counter-based (never derived from the prop name); // the hash KEY passed to `render_child` still carries the real name, // quoted via `jinjaHashKey`. test('a hyphenated prop name does not appear in the capture variable', () => { const { template } = compileAndGenerate(` function Card(props) { return null } export function Parent() { return Title}>text } `) expect(template).toContain('{% set bf_prop_0 %}') expect(template).toContain("'data-slot': bf_prop_0") expect(template).not.toContain('data-slot %}') expect(template).not.toContain('data-slot_') }) }) // #2038 nested-callback-predicate loudness is pinned at the shared // conformance layer (workstream C): `filter-nested-callback-predicate` / // `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and // `filter-nested-callback-predicate-client` (the `/* @client */` suppression // twin, which must render clean). // #2221: `_resolveLiteralConst` is a flat name lookup against // `ir.metadata.localConstants` with no notion of AST scope — it used to // substitute an outer const's literal value even at an occurrence that is // actually an enclosing loop callback's own (shadowing) parameter, so every // iteration rendered the same hard-coded literal. Guarded via the threaded, // position-accurate `BindingScope` (#2482 Stage 2 — previously a coarse // whole-component exclusion, same as #2212's, that also suppressed a // genuinely non-shadowed occurrence outside the loop; the threaded scope // only shadows AT the enclosing loop's own body). SSR-only tests for the // same #2222 reason as the #2212 describe below. describe('JinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => { test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => { const { template } = compileAndGenerate(` function Widget() { const label: string = 'x' return } `) // The loop body must reference the per-iteration loop var... expect(template).toContain('1 + label') // ...never the outer const's hard-coded value. expect(template).not.toContain("1 + 'x'") }) test('a numeric const shadowed by a loop param emits the identifier too', () => { const { template } = compileAndGenerate(` function Widget() { const count = 7 return } `) expect(template).toContain('1 + count') expect(template).not.toContain('1 + 7') }) test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => { const { template } = compileAndGenerate(` function Widget({ values }: { values: number[] }) { const totalPages = 5 return

Page 1 of {1 + totalPages}

} `) expect(template).toContain('1 + 5') }) // Position-accurate scope (#2482 Stage 2): a name that is loop-bound // elsewhere in the component but NOT at THIS occurrence still inlines // here — the coarse whole-component exclusion this used to hit (and // accept as a trade-off) is gone; only the loop's own body shadows. test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => { const { template } = compileAndGenerate(` function Widget({ values }: { values: number[] }) { const label: string = 'x' return

{1 + label}

} `) expect(template).toContain("1 + 'x'") expect(template).toContain('2 + label') }) }) // #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope // object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a // flat name lookup on `objectName` with no notion of AST scope, the // record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to // substitute the outer const's member value even at an occurrence that is // actually an enclosing loop callback's own (shadowing) parameter, so every // iteration rendered the same hard-coded literal instead of the per-item // value. Guarded via the threaded, position-accurate `BindingScope` (#2482 // Stage 2), same as #2221's fix above. describe('JinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => { test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => { const { template } = compileAndGenerate(` const cfg = { x: 'outer-lit' } function Widget({ rows }: { rows: { x: string }[] }) { return } `) // The loop body must reference the per-iteration member access... expect(template).toContain("bf.string(cfg['x'])") // ...never the outer const's hard-coded value. expect(template).not.toContain("bf.string('outer-lit')") }) test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => { const { template } = compileAndGenerate(` const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' } function Widget({ variant }: { variant: 'solid' | 'ghost' }) { return
{variantClasses.ghost}
} `) expect(template).toContain("bf.string('bg-ghost')") }) // Position-accurate scope (#2482 Stage 2): an object name that is // loop-bound elsewhere in the component but NOT at THIS occurrence still // inlines its member lookup here — the coarse whole-component exclusion // this used to hit (and accept as a trade-off) is gone; only the loop's // own body shadows. test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => { const { template } = compileAndGenerate(` const cfg = { x: 'outer-lit' } function Widget({ rows }: { rows: { x: string }[] }) { return

{cfg.x}

} `) expect(template).toContain("bf.string('outer-lit')") expect(template).toContain("bf.string(cfg['x'])") }) }) // Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate // `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set) // was only threaded around `renderChildren(loop.children)` in `renderLoop` // — narrower than the coarse set's always-on coverage. Two row-context // conversions sit OUTSIDE that window: a `.map()` preamble local's own // initializer, and the whole-item-conditional `loop-i:` key anchor // (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own // bindings — a loop param shadowing a same-named outer const must resolve // to the row value there too, exactly as it already does inside the loop // body proper. `renderLoop` now enters the row scope before converting // either and pops it after, closing the gap. describe('JinjaAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => { test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => { const { template } = compileAndGenerate(` function Widget({ values }: { values: number[] }) { const label: string = 'x' return } `) expect(template).toContain('bf.comment("loop-i:" ~ bf.escape_comment_key(label))') expect(template).not.toContain("bf.comment(\"loop-i:\" ~ bf.escape_comment_key('x'))") }) test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => { const { template } = compileAndGenerate(` function Widget({ values }: { values: number[] }) { const label: string = 'x' return } `) expect(template).toContain('{% set cls = label %}') expect(template).not.toContain("{% set cls = 'x' %}") }) }) // A fragment-rooted component (top-level `<>...` return) has no single // wrapping element to bound the client's scope query, so `renderFragment` // brackets its children with a comment-based scope marker pair instead of // element attributes. Without the end marker, a range query anchored on the // begin marker has no lower bound and leaks onto later siblings owned by // the parent (#2289). Mirrors the hono/xslate/mojo/go/erb adapters' fix — // see `wrapWithScopeComment` in packages/adapter-hono/src/adapter/hono-adapter.ts. describe('JinjaAdapter - fragment-root scope comment end marker (#2289)', () => { test('renderFragment brackets a multi-root fragment with paired begin/end scope_comment calls', () => { const { template } = compileAndGenerate(` export function ChildFragment() { return <>

hint

} `) expect(template).toContain('{{ bf.scope_comment() | safe }}') expect(template).toContain('{{ bf.scope_comment_end() | safe }}') // Begin marker precedes the children; end marker follows the fragment's // last top-level node -- both read the same `bf._scope_id()` at render // time, so the emitted scope ids are identical by construction. const beginIdx = template.indexOf('bf.scope_comment() | safe') const buttonIdx = template.indexOf('') const endIdx = template.indexOf('bf.scope_comment_end() | safe') expect(beginIdx).toBeLessThan(buttonIdx) expect(buttonIdx).toBeLessThan(pIdx) expect(pIdx).toBeLessThan(endIdx) // #2732: the fragment's FIRST element carries `data-key` — the wrapping // comment holds scope identity only, so the tag is no longer bare (which // is why the locator above matches ``). Pinned // both ways so "first element" can never silently widen to "every // element": a second carrier would put the loop key on two nodes and // `mapArray`'s `primaryEl.dataset.key` read would pick an arbitrary one. expect(template.slice(buttonIdx, pIdx)).toContain('data_key_attr') expect(template.slice(pIdx)).not.toContain('data_key_attr') }) test('a non-fragment (single element) root does NOT get scope_comment_end (element attrs bound the scope instead)', () => { const { template } = compileAndGenerate(` export function Child() { return
hi
} `) expect(template).not.toContain('scope_comment') }) }) describe('JinjaAdapter - scriptAssets (Vite late-binding, PR1)', () => { const CLIENT_COMPONENT = ` 'use client' import { createSignal } from '@barefootjs/client' export function Counter() { const [count, setCount] = createSignal(0) return } ` test('emits one register_script per URL, in order, when scriptAssets is set', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'], }) const runtimeIdx = template.indexOf("bf.register_script('/assets/runtime-abc123.js')") const compIdx = template.indexOf("bf.register_script('/assets/counter-def456.js')") expect(runtimeIdx).toBeGreaterThanOrEqual(0) expect(compIdx).toBeGreaterThanOrEqual(0) expect(runtimeIdx).toBeLessThan(compIdx) expect(template).toContain('_bf_reg0') expect(template).toContain('_bf_reg1') expect(template).not.toContain('/static/components/barefoot.js') expect(template).not.toContain('Counter.client.js') }) test('emits a single registration for a single-element scriptAssets array', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: ['/assets/only-one.js'], }) expect(template).toContain("bf.register_script('/assets/only-one.js')") expect(template.match(/register_script/g)?.length).toBe(1) }) test('an empty scriptAssets array emits no script registrations', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: [] }) expect(template).not.toContain('register_script') }) test('skipScriptRegistration still wins when scriptAssets is also set', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { skipScriptRegistration: true, scriptAssets: ['/assets/should-not-appear.js'], }) expect(template).not.toContain('register_script') }) test('absent scriptAssets falls back to adapter-computed script paths', () => { const ir = compileToIR(CLIENT_COMPONENT) const computed = new JinjaAdapter().generate(ir).template const explicitUndefined = new JinjaAdapter().generate(ir, { scriptAssets: undefined }).template expect(computed).toContain("bf.register_script('/static/components/barefoot.js')") expect(computed).toContain("bf.register_script('/static/components/Counter.client.js')") expect(explicitUndefined).toBe(computed) }) }) describe('JinjaAdapter - preloadAssets', () => { const CLIENT_COMPONENT = ` 'use client' import { createSignal } from '@barefootjs/client' export function Counter() { const [count, setCount] = createSignal(0) return } ` test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'], preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'], }) const pre1Idx = template.indexOf("{% set _bf_pre0 = bf.register_preload('/assets/index-pre1.js') %}") const pre2Idx = template.indexOf("{% set _bf_pre1 = bf.register_preload('/assets/shared-pre2.js') %}") const script1Idx = template.indexOf("{% set _bf_reg0 = bf.register_script('/assets/runtime-abc123.js') %}") const script2Idx = template.indexOf("{% set _bf_reg1 = bf.register_script('/assets/counter-def456.js') %}") expect(pre1Idx).toBeGreaterThanOrEqual(0) expect(pre2Idx).toBeGreaterThan(pre1Idx) expect(script1Idx).toBeGreaterThan(pre2Idx) expect(script2Idx).toBeGreaterThan(script1Idx) }) test('preloadAssets: [] emits no preload registration', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: ['/assets/runtime-abc123.js'], preloadAssets: [], }) expect(template).not.toContain('register_preload') expect(template).toContain("bf.register_script('/assets/runtime-abc123.js')") }) test('preloadAssets: undefined emits no preload registration', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: ['/assets/runtime-abc123.js'], preloadAssets: undefined, }) expect(template).not.toContain('register_preload') expect(template).toContain("bf.register_script('/assets/runtime-abc123.js')") }) test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: [], preloadAssets: ['/assets/index-pre1.js'], }) expect(template).not.toContain('register_preload') expect(template).not.toContain('register_script') }) test('skipScriptRegistration: true suppresses both preloads and scripts', () => { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { skipScriptRegistration: true, scriptAssets: ['/assets/runtime-abc123.js'], preloadAssets: ['/assets/index-pre1.js'], }) expect(template).not.toContain('register_preload') expect(template).not.toContain('register_script') }) // Regression guard: a previous attempt emitted a literal // `` tag directly into the component template, // which injected a rendered DOM node before the component's root and // broke hydration across all eight integrations (blade, erb, // go-template, jinja, mojolicious, rust, twig, xslate). Preload hints // must ONLY ever be emitted as no-output register statements (here, // `{% set _bf_preN = bf.register_preload(...) %}`) that the adapter's // runtime later renders itself — never as literal markup baked into the // template. test('never emits a literal { const ir = compileToIR(CLIENT_COMPONENT) const { template } = new JinjaAdapter().generate(ir, { scriptAssets: ['/assets/runtime-abc123.js'], preloadAssets: ['/assets/index-pre1.js'], }) expect(template).not.toContain('