@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jinja",
3
- "version": "0.33.4",
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.33.4"
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.33.4",
74
- "@barefootjs/vite": "0.33.4",
75
- "@barefootjs/client": "0.33.4",
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 componentName
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
- const cond = this.convertConditionToJinja(m.condition)
1416
- const val = this.convertExpressionToJinja(m.consequent)
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
- // Registered call lowerings (#2057) including the built-in `queryHref`
1760
- // plugin (#2042), which lowers `queryHref(base, { })` to a neutral
1761
- // `guard-list` on the `query` helper `bf.query(base, <triples>)`.
1762
- // Recognised before the support gate because the object-literal arg is
1763
- // otherwise `unsupported` (BF101). The `query` helper includes a pair iff its
1764
- // guard is truthy AND its value is a non-empty string (the client's
1765
- // `if (value)`): a plain `key: v` passes guard `true`, a conditional
1766
- // `key: cond ? v : undefined` passes the lowered cond. Only the `query`
1767
- // helper renders to `bf.query`; another guard-list helper must not be
1768
- // silently mis-rendered as a query.
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 support = pos === 'value' ? isSupportedValue(parsed) : isSupported(parsed)
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
 
@@ -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
+ }