@barefootjs/blade 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.
@@ -38,7 +38,7 @@
38
38
  * caller's `props` and needs no JSON round-trip.
39
39
  */
40
40
 
41
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
41
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
42
42
  import type { ComponentIR } from '@barefootjs/jsx'
43
43
  import { mkdir, rm } from 'node:fs/promises'
44
44
  import { resolve } from 'node:path'
@@ -157,8 +157,15 @@ export async function renderBladeComponent(options: RenderOptions): Promise<stri
157
157
  }
158
158
  }
159
159
 
160
- // Compile parent source.
161
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
160
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
161
+ // matches this harness's real behavior every sibling child template is registered
162
+ // alongside the parent before rendering, so a loop-body cross-template
163
+ // call resolves at render time (#2205).
164
+ const result = compileJSX(source, 'component.tsx', {
165
+ adapter,
166
+ outputIR: true,
167
+ siblingTemplatesRegistered: Boolean(components),
168
+ })
162
169
 
163
170
  const errors = result.errors.filter(e => e.severity === 'error')
164
171
  if (errors.length > 0) {
@@ -585,122 +592,6 @@ function buildBladeProps(
585
592
  return { data, literalAssignments }
586
593
  }
587
594
 
588
- /**
589
- * Evaluate a signal initializer expression using provided props.
590
- * Handles: props.initial ?? 0, props.value, literal values.
591
- *
592
- * Language-agnostic (returns a real JS value, not source text) — shared
593
- * verbatim with the Jinja harness's helper of the same name.
594
- */
595
- export function evaluateSignalInit(
596
- expr: string,
597
- props?: Record<string, unknown>,
598
- ): unknown {
599
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
600
- if (nullishMatch) {
601
- const propName = nullishMatch[1]
602
- const defaultExpr = nullishMatch[2].trim()
603
- if (props && propName in props) return props[propName]
604
- return parseLiteral(defaultExpr)
605
- }
606
-
607
- const propsMatch = expr.match(/^props\.(\w+)$/)
608
- if (propsMatch) {
609
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
610
- return null
611
- }
612
-
613
- return parseLiteral(expr)
614
- }
615
-
616
- function parseLiteral(expr: string): unknown {
617
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
618
- if (expr === 'true') return true
619
- if (expr === 'false') return false
620
- if (expr === '[]') return []
621
-
622
- {
623
- const t = expr.trim()
624
- if (t.startsWith('[') && t.endsWith(']')) {
625
- const inner = t.slice(1, -1).trim()
626
- if (!inner) return []
627
- const out: unknown[] = []
628
- for (const seg of splitTopLevelCommas(inner)) {
629
- if (!seg.trim()) continue
630
- const parsed = parseLiteral(seg.trim())
631
- if (parsed === null && seg.trim() !== 'null') return null
632
- out.push(parsed)
633
- }
634
- return out
635
- }
636
- }
637
-
638
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
639
- if (stringMatch) return unescapeJsString(stringMatch[2])
640
-
641
- const trimmed = expr.trim()
642
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
643
- const inner = trimmed.slice(1, -1).trim()
644
- if (!inner) return {}
645
- const obj: Record<string, unknown> = {}
646
- for (const pair of splitTopLevelCommas(inner)) {
647
- if (!pair.trim()) continue
648
- const colonIdx = pair.indexOf(':')
649
- if (colonIdx < 0) return null
650
- let key = pair.slice(0, colonIdx).trim()
651
- const val = pair.slice(colonIdx + 1).trim()
652
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
653
- if (keyMatch) key = unescapeJsString(keyMatch[2])
654
- const parsedVal = parseLiteral(val)
655
- if (parsedVal === null && val !== 'null') return null
656
- obj[key] = parsedVal
657
- }
658
- return obj
659
- }
660
- return null
661
- }
662
-
663
- function splitTopLevelCommas(inner: string): string[] {
664
- const segments: string[] = []
665
- let depth = 0
666
- let start = 0
667
- let quote: string | null = null
668
- for (let i = 0; i < inner.length; i++) {
669
- const c = inner[i]
670
- if (quote) {
671
- if (c === quote) {
672
- let backslashes = 0
673
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
674
- if (backslashes % 2 === 0) quote = null
675
- }
676
- continue
677
- }
678
- if (c === '"' || c === "'") {
679
- quote = c
680
- continue
681
- }
682
- if (c === '{' || c === '[') depth++
683
- else if (c === '}' || c === ']') depth--
684
- else if (c === ',' && depth === 0) {
685
- segments.push(inner.slice(start, i))
686
- start = i + 1
687
- }
688
- }
689
- segments.push(inner.slice(start))
690
- return segments
691
- }
692
-
693
- function unescapeJsString(s: string): string {
694
- return s.replace(/\\(.)/g, (_, c) => {
695
- switch (c) {
696
- case 'n': return '\n'
697
- case 'r': return '\r'
698
- case 't': return '\t'
699
- case '0': return '\0'
700
- default: return c
701
- }
702
- })
703
- }
704
595
 
705
596
  /**
706
597
  * Convert PascalCase to snake_case for template naming (matches the