@barefootjs/jinja 0.18.5 → 0.19.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.
@@ -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
- }