@barefootjs/blade 0.18.5 → 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 Jinja).
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 Jinja.
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 PHP array literal
23
+ // in the `@foreach` header, the same way a module-scope const's value is
24
+ // already seeded.
23
25
  // The `([emoji, users]) => …` array-destructure param itself now lowers
24
26
  // (#2087 Phase B — see the destructure comment below), but the loop
25
27
  // ARRAY is a function-scope computed const (`const entries =
@@ -28,12 +30,10 @@ export const conformancePins: ConformancePins = {
28
30
  // check and policy as Jinja / ERB) instead of silently iterating zero
29
31
  // times over an unbound name.
30
32
  'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
31
- // Both BF103 (imported child) and BF101 (computed local-const loop
32
- // array, as above) fire.
33
- 'static-array-from-props-with-component': [
34
- { code: 'BF103', severity: 'error' },
35
- { code: 'BF101', severity: 'error' },
36
- ],
33
+ // BF101 (computed local-const loop array, as above) fires; BF103
34
+ // (imported child in the loop body) no longer does now that the
35
+ // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
36
+ 'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
37
37
  // #2087 Phase B: every `.map()` destructure shape in the shared corpus
38
38
  // now lowers on Blade via an `@php(...)` local built from the binding's
39
39
  // structured `segments` path (`bladeLoopBindingAccessor` in
@@ -76,17 +76,14 @@ export const conformancePins: ConformancePins = {
76
76
  // / etc. via the same evaluator-JSON mechanism as `.filter` / `.every` /
77
77
  // `.some`, so they render. Only the NESTED-in-a-predicate form above is
78
78
  // refused (#2038).
79
- // #2073 follow-up (same as Jinja): a function-reference `.map(format)`
80
- // callback has no arrow body to serialize — not a CALLBACK_METHODS shape
81
- // (`asCallbackMethodCall` requires an arrow argument) so the shared
82
- // `isSupported`'s `UNSUPPORTED_METHODS` gate refuses it with the generic
83
- // "Expression not supported" BF101 rather than emitting a broken
84
- // template.
85
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
86
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
87
- // deliberate raw-HTML (unescaped) output affordance in the target
88
- // template language. No lowering exists yet, so the compiler refuses
89
- // the shape loudly instead of emitting entity-escaped markup that
90
- // silently renders tags as text.
91
- 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
79
+ // `array-map-function-reference` no longer pinned a bare-identifier
80
+ // `.map(format)` callback now resolves one hop to its declaration
81
+ // (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
82
+ // #2090 established for `.sort(fnref)`.
83
+ // `dangerous-inner-html` no longer pinned a compile-time string-literal
84
+ // `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
85
+ // the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
86
+ // A dynamic/signal-derived value still refuses with BF101 — see the
87
+ // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
88
+ 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
92
89
  }
@@ -14,4 +14,9 @@
14
14
 
15
15
  import type { RenderDivergences } from '@barefootjs/jsx'
16
16
 
17
- export const renderDivergences: RenderDivergences = {}
17
+ export const renderDivergences: RenderDivergences = {
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 }))`.
22
+ }
@@ -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