@barefootjs/jinja 0.33.4 → 0.34.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/dist/adapter/emit-context.d.ts +11 -1
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +11 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +33 -23
- package/dist/adapter/jinja-adapter.d.ts +25 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +38 -25
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +197 -135
- package/package.json +5 -5
- package/src/adapter/emit-context.ts +12 -1
- package/src/adapter/expr/emitters.ts +43 -0
- package/src/adapter/jinja-adapter.ts +54 -39
- package/src/conformance-pins.ts +11 -0
- package/src/render-divergences.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jinja",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"directory": "packages/adapter-jinja"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.
|
|
56
|
+
"@barefootjs/shared": "0.34.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
73
|
-
"@barefootjs/jsx": "0.
|
|
74
|
-
"@barefootjs/vite": "0.
|
|
75
|
-
"@barefootjs/client": "0.
|
|
73
|
+
"@barefootjs/jsx": "0.34.0",
|
|
74
|
+
"@barefootjs/vite": "0.34.0",
|
|
75
|
+
"@barefootjs/client": "0.34.0",
|
|
76
76
|
"typescript": "^5.0.0",
|
|
77
77
|
"vite": "^6.0.0"
|
|
78
78
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* rather than re-exposing the whole adapter.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
|
|
22
|
+
import type { ParsedExpr, CompilerError, IRMetadata, LoweringMatcher } from '@barefootjs/jsx'
|
|
23
23
|
|
|
24
24
|
export interface JinjaEmitContext {
|
|
25
25
|
/**
|
|
@@ -28,6 +28,17 @@ export interface JinjaEmitContext {
|
|
|
28
28
|
*/
|
|
29
29
|
readonly _searchParamsLocals: Set<string>
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Registered lowering-plugin matchers (#2057), bound to this component's
|
|
33
|
+
* metadata at init. Read by `JinjaTopLevelEmitter`'s `lowering` seam
|
|
34
|
+
* (#2843) so a registered call — the built-in `queryHref`, or any
|
|
35
|
+
* userland plugin — is recognised no matter where it sits in an
|
|
36
|
+
* expression tree (a ternary branch, a template-literal interpolation, …),
|
|
37
|
+
* not only when it's the call the adapter's own top-level conversion
|
|
38
|
+
* entry point (`convertExpressionToJinja`) is asked to lower directly.
|
|
39
|
+
*/
|
|
40
|
+
readonly _loweringMatchers: readonly LoweringMatcher[]
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Inline a module-scope pure string-literal const by name as the resolved
|
|
33
44
|
* literal value, or null when the name is not such a const.
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
import { groupBinaryOperand,
|
|
52
52
|
groupObjectLiteralSegments,
|
|
53
53
|
type ParsedExprEmitter,
|
|
54
|
+
type LoweringEmitter,
|
|
55
|
+
type LoweringNode,
|
|
54
56
|
type HigherOrderMethod,
|
|
55
57
|
type ArrayMethod,
|
|
56
58
|
type LiteralType,
|
|
@@ -61,6 +63,8 @@ import { groupBinaryOperand,
|
|
|
61
63
|
identifierPath,
|
|
62
64
|
matchSearchParamsMethodCall,
|
|
63
65
|
sortComparatorFromArrow,
|
|
66
|
+
queryHrefArgs,
|
|
67
|
+
isValidHelperId,
|
|
64
68
|
} from '@barefootjs/jsx'
|
|
65
69
|
|
|
66
70
|
import type { JinjaEmitContext } from '../emit-context.ts'
|
|
@@ -323,6 +327,45 @@ export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
|
323
327
|
this.ctx = ctx
|
|
324
328
|
}
|
|
325
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Registered-lowering seam (#2843): `emitParsedExpr`'s shared `call` case
|
|
332
|
+
* tries every matcher here BEFORE `call()` itself, so a registered call
|
|
333
|
+
* (the built-in `queryHref`, or any userland plugin) is recognised no
|
|
334
|
+
* matter where it sits in the tree. `render` is what used to live inline
|
|
335
|
+
* in `JinjaAdapter.convertExpressionToJinja` before the object-literal
|
|
336
|
+
* support-gate refusal (`checkSupport`'s `call` arm, now itself
|
|
337
|
+
* registry-aware) made the pre-gate special case unnecessary.
|
|
338
|
+
*/
|
|
339
|
+
get lowering(): LoweringEmitter {
|
|
340
|
+
return {
|
|
341
|
+
matchers: this.ctx._loweringMatchers,
|
|
342
|
+
render: (node: LoweringNode, emit: (e: ParsedExpr) => string): string | null => {
|
|
343
|
+
// `query` guard-list — `queryHref`-shaped. The helper includes a
|
|
344
|
+
// pair iff its guard is truthy AND its value is a non-empty string
|
|
345
|
+
// (the client's `if (value)`): a plain `key: v` passes guard
|
|
346
|
+
// `true`, a conditional `key: cond ? v : undefined` passes the
|
|
347
|
+
// lowered cond. Only the `query` helper renders to `bf.query`;
|
|
348
|
+
// another guard-list helper must not be silently mis-rendered as
|
|
349
|
+
// a query.
|
|
350
|
+
if (node.kind === 'guard-list' && node.helper === 'query') {
|
|
351
|
+
const qArgs = queryHrefArgs(node, emit)
|
|
352
|
+
return `bf.query(${qArgs.join(', ')})`
|
|
353
|
+
}
|
|
354
|
+
// Generic `helper-call` (#2069) — the neutral vocabulary's escape
|
|
355
|
+
// hatch for a userland `LoweringPlugin` that lowers to a single
|
|
356
|
+
// runtime-helper invocation. `bf.<helper>(args…)` mirrors the
|
|
357
|
+
// `query` helper's own naming convention exactly: the framework
|
|
358
|
+
// renders the call, the plugin author registers `<helper>` as a
|
|
359
|
+
// Jinja-callable function/filter in their own runtime — same
|
|
360
|
+
// contract as `bf.query` itself, just not built in.
|
|
361
|
+
if (node.kind === 'helper-call' && isValidHelperId(node.helper)) {
|
|
362
|
+
return `bf.${node.helper}(${node.args.map(emit).join(', ')})`
|
|
363
|
+
}
|
|
364
|
+
return null
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
326
369
|
identifier(name: string): string {
|
|
327
370
|
// `undefined` / `null` nested inside a larger expression tree — Jinja
|
|
328
371
|
// `none` (#1897).
|
|
@@ -138,8 +138,6 @@ import {
|
|
|
138
138
|
lookupStaticRecordLiteral,
|
|
139
139
|
searchParamsLocalNames,
|
|
140
140
|
prepareLoweringMatchers,
|
|
141
|
-
queryHrefArgs,
|
|
142
|
-
isValidHelperId,
|
|
143
141
|
sortComparatorFromArrow,
|
|
144
142
|
isDangerousInnerHtmlAttr,
|
|
145
143
|
resolveDangerousInnerHtml,
|
|
@@ -148,6 +146,7 @@ import {
|
|
|
148
146
|
resolveStaticLoopSource,
|
|
149
147
|
derivesScopeFromSlot,
|
|
150
148
|
BindingScope,
|
|
149
|
+
buildImportAliasMap,
|
|
151
150
|
} from '@barefootjs/jsx'
|
|
152
151
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
153
152
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
@@ -285,6 +284,18 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
285
284
|
*/
|
|
286
285
|
private nullableOptionalProps: Set<string> = new Set()
|
|
287
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Local alias -> declared/exported name for imported components (#2822,
|
|
289
|
+
* the SSR-side counterpart of #2777's client-JS registry-key fix). A
|
|
290
|
+
* child referenced under an import alias (`import { Foo as Bar }`,
|
|
291
|
+
* `<Bar/>`) must build its cross-template `render_child` call against
|
|
292
|
+
* the child's own declared name (`Foo`, what `foo.tsx` registers its
|
|
293
|
+
* Jinja partial as) — never the caller-local binding. Built once per
|
|
294
|
+
* compile from `ir.metadata.imports` via the shared `buildImportAliasMap`
|
|
295
|
+
* (`@barefootjs/jsx`) and read by `toTemplateName`.
|
|
296
|
+
*/
|
|
297
|
+
private importAliases: Map<string, string> = new Map()
|
|
298
|
+
|
|
288
299
|
constructor(options: JinjaAdapterOptions = {}) {
|
|
289
300
|
super()
|
|
290
301
|
this.options = {
|
|
@@ -313,6 +324,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
313
324
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
314
325
|
this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
315
326
|
this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
327
|
+
this.importAliases = buildImportAliasMap(ir.metadata.imports ?? [])
|
|
316
328
|
this.errors = []
|
|
317
329
|
this.childrenCaptureCounter = 0
|
|
318
330
|
|
|
@@ -1256,8 +1268,12 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1256
1268
|
private presenceVarCounter = 0
|
|
1257
1269
|
|
|
1258
1270
|
private toTemplateName(componentName: string): string {
|
|
1271
|
+
// Resolve an import alias (`import { Foo as Bar }`, `<Bar/>`) back to
|
|
1272
|
+
// the child's own declared name BEFORE snake-casing (#2822) — `Bar`
|
|
1273
|
+
// has no `foo.tsx`-registered partial; only `Foo` does.
|
|
1274
|
+
const declaredName = this.importAliases.get(componentName) ?? componentName
|
|
1259
1275
|
// Convert PascalCase to snake_case for template naming.
|
|
1260
|
-
return
|
|
1276
|
+
return declaredName
|
|
1261
1277
|
.replace(/([A-Z])/g, '_$1')
|
|
1262
1278
|
.toLowerCase()
|
|
1263
1279
|
.replace(/^_/, '')
|
|
@@ -1412,8 +1428,13 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1412
1428
|
{
|
|
1413
1429
|
const m = this.parseUndefinedAlternateTernary(value.expr)
|
|
1414
1430
|
if (m) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1431
|
+
// Pass the PARSED sub-trees through as `preParsed` (#2843 review)
|
|
1432
|
+
// rather than re-parsing `m.condition`/`m.consequent` — those are
|
|
1433
|
+
// `exprToString` debug text, which renders any nested
|
|
1434
|
+
// `object-literal` (e.g. a registered call's params, `queryHref`'s
|
|
1435
|
+
// `{ tag }`) as a non-reparseable `[UNSUPPORTED: …]` placeholder.
|
|
1436
|
+
const cond = this.convertConditionToJinja(m.condition, m.testParsed)
|
|
1437
|
+
const val = this.convertExpressionToJinja(m.consequent, m.consequentParsed)
|
|
1417
1438
|
return `\n{% if ${cond} %}\n${name}="{{ bf.string(${val}) }}"\n{% endif %}\n`
|
|
1418
1439
|
}
|
|
1419
1440
|
}
|
|
@@ -1675,7 +1696,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1675
1696
|
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe)
|
|
1676
1697
|
if (!startsAsObjectLiteral && !hasTaggedTemplate) return false
|
|
1677
1698
|
const parsed = parseExpression(expr.trim())
|
|
1678
|
-
const support = isSupported(parsed)
|
|
1699
|
+
const support = isSupported(parsed, { loweringMatchers: this._loweringMatchers })
|
|
1679
1700
|
if (parsed.kind !== 'unsupported' && support.supported) return false
|
|
1680
1701
|
const reason = support.reason ?? (parsed.kind === 'unsupported' ? parsed.reason : undefined)
|
|
1681
1702
|
const reasonLine = reason ? `\n${reason}` : ''
|
|
@@ -1702,6 +1723,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1702
1723
|
private get emitCtx(): JinjaEmitContext {
|
|
1703
1724
|
return {
|
|
1704
1725
|
_searchParamsLocals: this._searchParamsLocals,
|
|
1726
|
+
_loweringMatchers: this._loweringMatchers,
|
|
1705
1727
|
_resolveModuleStringConst: (name) => this._resolveModuleStringConst(name),
|
|
1706
1728
|
_resolveLiteralConst: (name) => this._resolveLiteralConst(name),
|
|
1707
1729
|
_resolveStaticRecordLiteral: (o, k) => this._resolveStaticRecordLiteral(o, k),
|
|
@@ -1756,42 +1778,22 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1756
1778
|
parsed = parseExpression(trimmed)
|
|
1757
1779
|
}
|
|
1758
1780
|
|
|
1759
|
-
//
|
|
1760
|
-
// plugin
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1764
|
-
//
|
|
1765
|
-
//
|
|
1766
|
-
// `
|
|
1767
|
-
//
|
|
1768
|
-
//
|
|
1769
|
-
if (parsed.kind === 'call') {
|
|
1770
|
-
for (const matcher of this._loweringMatchers) {
|
|
1771
|
-
const node = matcher(parsed.callee, parsed.args)
|
|
1772
|
-
if (node?.kind === 'guard-list' && node.helper === 'query') {
|
|
1773
|
-
const qArgs = queryHrefArgs(node, n => this.renderParsedExprToJinja(n))
|
|
1774
|
-
return `bf.query(${qArgs.join(', ')})`
|
|
1775
|
-
}
|
|
1776
|
-
// Generic `helper-call` (#2069) — the neutral vocabulary's escape
|
|
1777
|
-
// hatch for a userland `LoweringPlugin` that lowers to a single
|
|
1778
|
-
// runtime-helper invocation. `bf.<helper>(args…)` mirrors the
|
|
1779
|
-
// `query` helper's own naming convention exactly: the framework
|
|
1780
|
-
// renders the call, the plugin author registers `<helper>` as a
|
|
1781
|
-
// Jinja-callable function/filter in their own runtime — same
|
|
1782
|
-
// contract as `bf.query` itself, just not built in.
|
|
1783
|
-
if (node?.kind === 'helper-call' && isValidHelperId(node.helper)) {
|
|
1784
|
-
const argsX = node.args.map(a => this.renderParsedExprToJinja(a))
|
|
1785
|
-
return `bf.${node.helper}(${argsX.join(', ')})`
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1781
|
+
// #2843: a registered lowering plugin's call (the built-in `queryHref`,
|
|
1782
|
+
// or any userland plugin) is recognised no matter where it sits in the
|
|
1783
|
+
// tree — a ternary branch, a template-literal interpolation, … — not
|
|
1784
|
+
// only when `parsed.kind === 'call'` directly. That recognition now
|
|
1785
|
+
// lives in `JinjaTopLevelEmitter`'s `lowering` seam, consulted by
|
|
1786
|
+
// `emitParsedExpr`'s shared `call` dispatch; the support gate below is
|
|
1787
|
+
// passed `this._loweringMatchers` so a matched call's params (e.g.
|
|
1788
|
+
// `queryHref`'s object literal, otherwise `unsupported` at `rendered`
|
|
1789
|
+
// position) are admitted wherever the call is nested.
|
|
1790
|
+
//
|
|
1790
1791
|
// `pos` distinguishes a derived-seed RHS (an assignment, checked via
|
|
1791
1792
|
// `isSupportedValue`) from every other, genuinely rendered call site —
|
|
1792
1793
|
// the rendered gate would otherwise re-refuse a tree the seed plan
|
|
1793
1794
|
// already classified `derived` at value position (#2696 review).
|
|
1794
|
-
const
|
|
1795
|
+
const supportOpts = { loweringMatchers: this._loweringMatchers }
|
|
1796
|
+
const support = pos === 'value' ? isSupportedValue(parsed, supportOpts) : isSupported(parsed, supportOpts)
|
|
1795
1797
|
if (!support.supported) {
|
|
1796
1798
|
this.errors.push({
|
|
1797
1799
|
code: 'BF101',
|
|
@@ -1857,10 +1859,21 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1857
1859
|
* Parse `cond ? value : undefined` (or `: null`), returning the
|
|
1858
1860
|
* condition/consequent source spans, else `null`. Used for the
|
|
1859
1861
|
* attribute-omission rule (#1897).
|
|
1862
|
+
*
|
|
1863
|
+
* `testParsed`/`consequentParsed` (#2843 review) are the ACTUAL parsed
|
|
1864
|
+
* sub-trees, for the caller to pass through as `preParsed` — `condition`/
|
|
1865
|
+
* `consequent` are `exprToString` output, a DEBUG formatter that renders
|
|
1866
|
+
* any nested `object-literal` as the non-reparseable `[UNSUPPORTED: …]`
|
|
1867
|
+
* placeholder (unlike `stringifyParsedExpr`, meant for round-tripping).
|
|
1868
|
+
* Re-parsing THAT string for a consequent like `queryHref(base, { tag })`
|
|
1869
|
+
* would corrupt the call's args before the lowering registry — now
|
|
1870
|
+
* consulted at any nesting depth (#2843) — ever sees them. Kept for any
|
|
1871
|
+
* caller that only needs the string form; the parsed trees are the
|
|
1872
|
+
* correct input wherever the caller can accept `preParsed`.
|
|
1860
1873
|
*/
|
|
1861
1874
|
parseUndefinedAlternateTernary(
|
|
1862
1875
|
expr: string,
|
|
1863
|
-
): { condition: string; consequent: string } | null {
|
|
1876
|
+
): { condition: string; consequent: string; testParsed: ParsedExpr; consequentParsed: ParsedExpr } | null {
|
|
1864
1877
|
const parsed = parseExpression(expr.trim())
|
|
1865
1878
|
if (parsed?.kind !== 'conditional') return null
|
|
1866
1879
|
const alt = parsed.alternate
|
|
@@ -1875,6 +1888,8 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1875
1888
|
return {
|
|
1876
1889
|
condition: exprToString(parsed.test),
|
|
1877
1890
|
consequent: exprToString(parsed.consequent),
|
|
1891
|
+
testParsed: parsed.test,
|
|
1892
|
+
consequentParsed: parsed.consequent,
|
|
1878
1893
|
}
|
|
1879
1894
|
}
|
|
1880
1895
|
|
package/src/conformance-pins.ts
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
11
11
|
|
|
12
12
|
export const conformancePins: ConformancePins = {
|
|
13
|
+
// #2843: graduated — a registered lowering call inside a ternary
|
|
14
|
+
// attribute branch (or any nested value position) is now recognised via
|
|
15
|
+
// `JinjaTopLevelEmitter`'s `lowering` seam + the registry-aware support
|
|
16
|
+
// gate, matching the direct-call attribute path exactly.
|
|
13
17
|
'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
|
|
14
18
|
'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
|
|
15
19
|
'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
|
|
@@ -63,4 +67,11 @@ export const conformancePins: ConformancePins = {
|
|
|
63
67
|
// `rich-prop-client-read` above.
|
|
64
68
|
'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
|
|
65
69
|
'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
|
|
70
|
+
// #2771: a reactive primitive invoked through a namespace import
|
|
71
|
+
// (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
|
|
72
|
+
// that the analyzer's checker-less fast path cannot recognize refuses
|
|
73
|
+
// loudly (BF013) instead of silently dropping the declaration — fired
|
|
74
|
+
// in the shared analyzer pass ahead of any adapter's `adapter.generate()`,
|
|
75
|
+
// so all nine adapters (including Hono) pin this identically.
|
|
76
|
+
'namespace-import-primitive': [{ code: 'BF013', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2771' }],
|
|
66
77
|
}
|
|
@@ -15,4 +15,7 @@ import type { RenderDivergences } from '@barefootjs/jsx'
|
|
|
15
15
|
// at value position and the runtime evaluator's `object-literal` case
|
|
16
16
|
// now merges it, so the seed classifies `derived` and SSRs identically
|
|
17
17
|
// to Hono.
|
|
18
|
-
export const renderDivergences: RenderDivergences = {
|
|
18
|
+
export const renderDivergences: RenderDivergences = {
|
|
19
|
+
'aliased-loop-source':
|
|
20
|
+
'A `.map()` loop whose source is a local const alias of a signal getter (`const items__alias = items`) SSRs an empty `<ul>` on real Jinja2 — the seeded loop data is keyed by the signal\'s real name (`items`), and the alias hop is never resolved when deciding what to seed under `items__alias`. This is the SSR-side twin of #2778 (fixed for the CSR client-JS template in the same PR that added this fixture) — that fix only touches client-JS emission, not SSR data-seeding. Tracked at https://github.com/piconic-ai/barefootjs/issues/2813; graduate by resolving the alias hop at SSR-seeding time using the same `resolveAliasOrigin`/`resolveGetterAliases` mechanism #2778 introduced, rather than a third alias-hop walker.',
|
|
21
|
+
}
|