@barefootjs/jinja 0.18.4 → 0.18.7

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.
@@ -11,15 +11,17 @@
11
11
  import type { ConformancePins } from '@barefootjs/jsx'
12
12
 
13
13
  export const conformancePins: ConformancePins = {
14
- // Sibling-imported child component in a loop body: emits a
15
- // cross-template call needing separate registration. BF103 makes
16
- // the requirement loud (same as xslate).
17
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
18
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
19
- // call it inside a keyed `.map`. Same BF103 (imported child in
20
- // `.map`) as xslate.
21
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
22
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
14
+ // `todo-app` / `todo-app-ssr` no longer pinned (#2205) the conformance
15
+ // harness now passes `siblingTemplatesRegistered: true` for fixtures with
16
+ // sibling `components`, matching `bf build`'s real semantics, so the
17
+ // BF103 loop-body cross-template check no longer fires spuriously. (Both
18
+ // fixtures are still skipped on this adapter via `render-divergences.ts`
19
+ // #2209 for an unrelated signal-seeding gap.)
20
+ // `static-array-children` no longer pinned (#2208) `items`'s
21
+ // array-literal initializer is now recognized as fully-static
22
+ // (`resolveStaticLoopSource`) and inlined as a native Jinja list/dict
23
+ // literal in the `{% for %}` header, the same way a module-scope const's
24
+ // value is already seeded.
23
25
  // #2087 Phase A/B widened the destructure gate (`isLowerableLoopDestructure`)
24
26
  // to admit array-index / nested-path fixed bindings, so the
25
27
  // `([emoji, users]) => ...` / `([id, t]) => ...` params in these two
@@ -44,10 +46,10 @@ export const conformancePins: ConformancePins = {
44
46
  'static-array-from-props': [
45
47
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
46
48
  ],
47
- // Both BF103 (imported child) and BF101 (unresolvable computed loop
48
- // array, see above) fire.
49
+ // BF101 (unresolvable computed loop array, see above) fires; BF103
50
+ // (imported child in the loop body) no longer does now that the
51
+ // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
49
52
  'static-array-from-props-with-component': [
50
- { code: 'BF103', severity: 'error' },
51
53
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
52
54
  ],
53
55
  // Rest-destructure / structured-path `.map()` callbacks (#2087 Phase B):
@@ -91,22 +93,14 @@ export const conformancePins: ConformancePins = {
91
93
  // / etc. via the same evaluator-JSON mechanism as `.filter` / `.every` /
92
94
  // `.some`, so they render. Only the NESTED-in-a-predicate form above is
93
95
  // refused (#2038).
94
- // #2073 follow-up (same as xslate): a function-reference `.map(format)`
95
- // callback has no arrow body to serialize — not a CALLBACK_METHODS shape
96
- // (`asCallbackMethodCall` requires an arrow argument) so the shared
97
- // `isSupported`'s `UNSUPPORTED_METHODS` gate refuses it with the generic
98
- // "Expression not supported" BF101 rather than emitting a broken
99
- // template.
100
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
101
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
102
- // deliberate raw-HTML (unescaped) output affordance in the target
103
- // template language. No lowering exists yet, so the compiler refuses
104
- // the shape loudly instead of emitting entity-escaped markup that
105
- // silently renders tags as text.
106
- 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
107
- // Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
108
- // only first-occurrence `.replace` is wired to the runtime helpers.
109
- // Refused with BF101 rather than reusing the first-only lowering,
110
- // which would silently change semantics.
111
- 'string-replaceall': [{ code: 'BF101', severity: 'error' }],
96
+ // `array-map-function-reference` no longer pinned a bare-identifier
97
+ // `.map(format)` callback now resolves one hop to its declaration
98
+ // (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
99
+ // #2090 established for `.sort(fnref)`.
100
+ // `dangerous-inner-html` no longer pinned a compile-time string-literal
101
+ // `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
102
+ // the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
103
+ // A dynamic/signal-derived value still refuses with BF101 — see the
104
+ // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
105
+ 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
112
106
  }
@@ -15,20 +15,8 @@
15
15
  import type { RenderDivergences } from '@barefootjs/jsx'
16
16
 
17
17
  export const renderDivergences: RenderDivergences = {
18
- 'html-entity-text':
19
- '`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
20
- 'math-methods':
21
- 'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
22
- 'static-attr-escape':
23
- 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
24
- 'object-entries-map':
25
- '`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
26
- 'nested-loop-outer-binding':
27
- 'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
28
- 'jsx-element-prop':
29
- 'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
30
- 'string-slice':
31
- '`.slice()` on a STRING renders empty (array-slice helper misfires on strings)',
32
- 'string-trim-sided':
33
- '`.trimStart()` / `.trimEnd()` render empty (no lowering)',
18
+ // `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
19
+ // `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
20
+ // instead of a fixed regex-shape catalogue) now correctly seeds `todos`
21
+ // from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
34
22
  }
@@ -16,7 +16,7 @@
16
16
  * generated render script (Python, not Perl) and its literal syntax differ.
17
17
  */
18
18
 
19
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
19
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
20
20
  import type { ComponentIR } from '@barefootjs/jsx'
21
21
  import { mkdir, rm } from 'node:fs/promises'
22
22
  import { resolve } from 'node:path'
@@ -125,8 +125,15 @@ export async function renderJinjaComponent(options: RenderOptions): Promise<stri
125
125
  }
126
126
  }
127
127
 
128
- // Compile parent source.
129
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
128
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
129
+ // matches this harness's real behavior every sibling child template is registered
130
+ // alongside the parent before rendering, so a loop-body cross-template
131
+ // call resolves at render time (#2205).
132
+ const result = compileJSX(source, 'component.tsx', {
133
+ adapter,
134
+ outputIR: true,
135
+ siblingTemplatesRegistered: Boolean(components),
136
+ })
130
137
 
131
138
  const errors = result.errors.filter(e => e.severity === 'error')
132
139
  if (errors.length > 0) {
@@ -461,9 +468,9 @@ function buildPythonProps(
461
468
  for (const param of ir.metadata.propsParams) {
462
469
  if (props && param.name in props) continue
463
470
  if (param.defaultValue) {
464
- const pyValue = jsToPyValue(param.defaultValue)
465
- if (pyValue !== null) {
466
- entries.push(`${pyStr(param.name)}: ${pyValue}`)
471
+ const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
472
+ if (result.ok) {
473
+ entries.push(`${pyStr(param.name)}: ${toPyLiteral(result.value)}`)
467
474
  continue
468
475
  }
469
476
  }
@@ -539,119 +546,6 @@ function buildPythonProps(
539
546
  return `{${entries.join(', ')}}`
540
547
  }
541
548
 
542
- /**
543
- * Evaluate a signal initializer expression using provided props.
544
- * Handles: props.initial ?? 0, props.value, literal values.
545
- */
546
- export function evaluateSignalInit(
547
- expr: string,
548
- props?: Record<string, unknown>,
549
- ): unknown {
550
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
551
- if (nullishMatch) {
552
- const propName = nullishMatch[1]
553
- const defaultExpr = nullishMatch[2].trim()
554
- if (props && propName in props) return props[propName]
555
- return parseLiteral(defaultExpr)
556
- }
557
-
558
- const propsMatch = expr.match(/^props\.(\w+)$/)
559
- if (propsMatch) {
560
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
561
- return null
562
- }
563
-
564
- return parseLiteral(expr)
565
- }
566
-
567
- function parseLiteral(expr: string): unknown {
568
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
569
- if (expr === 'true') return true
570
- if (expr === 'false') return false
571
- if (expr === '[]') return []
572
-
573
- {
574
- const t = expr.trim()
575
- if (t.startsWith('[') && t.endsWith(']')) {
576
- const inner = t.slice(1, -1).trim()
577
- if (!inner) return []
578
- const out: unknown[] = []
579
- for (const seg of splitTopLevelCommas(inner)) {
580
- if (!seg.trim()) continue
581
- const parsed = parseLiteral(seg.trim())
582
- if (parsed === null && seg.trim() !== 'null') return null
583
- out.push(parsed)
584
- }
585
- return out
586
- }
587
- }
588
-
589
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
590
- if (stringMatch) return unescapeJsString(stringMatch[2])
591
-
592
- const trimmed = expr.trim()
593
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
594
- const inner = trimmed.slice(1, -1).trim()
595
- if (!inner) return {}
596
- const obj: Record<string, unknown> = {}
597
- for (const pair of splitTopLevelCommas(inner)) {
598
- if (!pair.trim()) continue
599
- const colonIdx = pair.indexOf(':')
600
- if (colonIdx < 0) return null
601
- let key = pair.slice(0, colonIdx).trim()
602
- const val = pair.slice(colonIdx + 1).trim()
603
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
604
- if (keyMatch) key = unescapeJsString(keyMatch[2])
605
- const parsedVal = parseLiteral(val)
606
- if (parsedVal === null && val !== 'null') return null
607
- obj[key] = parsedVal
608
- }
609
- return obj
610
- }
611
- return null
612
- }
613
-
614
- function splitTopLevelCommas(inner: string): string[] {
615
- const segments: string[] = []
616
- let depth = 0
617
- let start = 0
618
- let quote: string | null = null
619
- for (let i = 0; i < inner.length; i++) {
620
- const c = inner[i]
621
- if (quote) {
622
- if (c === quote) {
623
- let backslashes = 0
624
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
625
- if (backslashes % 2 === 0) quote = null
626
- }
627
- continue
628
- }
629
- if (c === '"' || c === "'") {
630
- quote = c
631
- continue
632
- }
633
- if (c === '{' || c === '[') depth++
634
- else if (c === '}' || c === ']') depth--
635
- else if (c === ',' && depth === 0) {
636
- segments.push(inner.slice(start, i))
637
- start = i + 1
638
- }
639
- }
640
- segments.push(inner.slice(start))
641
- return segments
642
- }
643
-
644
- function unescapeJsString(s: string): string {
645
- return s.replace(/\\(.)/g, (_, c) => {
646
- switch (c) {
647
- case 'n': return '\n'
648
- case 'r': return '\r'
649
- case 't': return '\t'
650
- case '0': return '\0'
651
- default: return c
652
- }
653
- })
654
- }
655
549
 
656
550
  /**
657
551
  * Python string literal for arbitrary text, via `JSON.stringify`. JSON's
@@ -689,26 +583,3 @@ function toPyLiteral(value: unknown): string {
689
583
  return 'None'
690
584
  }
691
585
 
692
- /**
693
- * Convert a JS literal value to a Python literal.
694
- * Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default.
695
- */
696
- function jsToPyValue(jsValue: string): string | null {
697
- const v = jsValue.trim()
698
-
699
- if (/^-?\d+(\.\d+)?$/.test(v)) return v
700
- // A JS string literal (single- or double-quoted) is, character-for-character,
701
- // ALSO a valid Python string literal for the common escape sequences both
702
- // languages share — pass it through verbatim rather than re-quoting.
703
- if (/^['"].*['"]$/.test(v)) return v
704
- if (v === 'true') return 'True'
705
- if (v === 'false') return 'False'
706
- if (v === '[]') return '[]'
707
-
708
- const nullishMatch = v.match(/\?\?\s*(.+)$/)
709
- if (nullishMatch) return jsToPyValue(nullishMatch[1])
710
-
711
- if (v.startsWith('props.')) return 'None'
712
-
713
- return null
714
- }