@barefootjs/erb 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.
@@ -12,16 +12,17 @@
12
12
  import type { ConformancePins } from '@barefootjs/jsx'
13
13
 
14
14
  export const conformancePins: ConformancePins = {
15
- // Sibling-imported child component in a loop body: emits a
16
- // cross-template call needing separate registration. BF103 makes
17
- // the requirement loud (same as mojo).
18
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
19
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
20
- // call it inside a keyed `.map`. Same BF103 surface as the
21
- // synthetic `static-array-children` above pinned at adapter
22
- // level so the shared-component corpus stays adapter-neutral.
23
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
24
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
15
+ // `todo-app` / `todo-app-ssr` no longer pinned (#2205) the conformance
16
+ // harness now passes `siblingTemplatesRegistered: true` for fixtures with
17
+ // sibling `components`, matching `bf build`'s real semantics, so the
18
+ // BF103 loop-body cross-template check no longer fires spuriously. (Both
19
+ // fixtures are still skipped on this adapter via `render-divergences.ts`
20
+ // #2209 for an unrelated signal-seeding gap.)
21
+ // `static-array-children` no longer pinned (#2208) — `items`'s
22
+ // array-literal initializer is now recognized as fully-static
23
+ // (`resolveStaticLoopSource`) and inlined as a native Ruby array/hash
24
+ // literal in the loop-bound expression, the same way a module-scope
25
+ // const's value is already seeded.
25
26
  // `static-array-from-props` / `static-array-from-props-with-component`:
26
27
  // the `.map(([emoji, users]) => …)` / `.map(([id, t]) => …)` callback is
27
28
  // a plain array-index destructure (the `.filter(...)` runs on a
@@ -45,8 +46,9 @@ export const conformancePins: ConformancePins = {
45
46
  'static-array-from-props': [
46
47
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
47
48
  ],
49
+ // BF103 (imported child in the loop body) no longer fires now that the
50
+ // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
48
51
  'static-array-from-props-with-component': [
49
- { code: 'BF103', severity: 'error' },
50
52
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
51
53
  ],
52
54
  // #2087 Phase B: `isLowerableLoopDestructure` now admits every fixed-
@@ -91,16 +93,14 @@ export const conformancePins: ConformancePins = {
91
93
  // `/* @client */` keyed-map slot-id elision contract only (same as
92
94
  // `todo-app`), not a render or BF101 gap.
93
95
  //
94
- // #2073 follow-up: a function-reference `.map(format)` callback has no
95
- // arrow body to serialize not a CALLBACK_METHODS shape — so the
96
- // UNSUPPORTED_METHODS gate (shared `@barefootjs/jsx` code) refuses it
97
- // with BF101 rather than emitting a broken template. Same pin as
98
- // mojo/xslate.
99
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
100
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
101
- // deliberate raw-HTML (unescaped) output affordance in the target
102
- // template language. No lowering exists yet, so the compiler refuses
103
- // the shape loudly instead of emitting entity-escaped markup that
104
- // silently renders tags as text.
105
- 'dangerous-inner-html': [{ 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' }],
106
106
  }
@@ -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
+ }
@@ -36,7 +36,7 @@
36
36
  * regression test.
37
37
  */
38
38
 
39
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
39
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
40
40
  import type { ComponentIR, SsrDefault } from '@barefootjs/jsx'
41
41
  import { mkdir, rm } from 'node:fs/promises'
42
42
  import { resolve } from 'node:path'
@@ -151,8 +151,15 @@ export async function renderErbComponent(options: RenderOptions): Promise<string
151
151
  }
152
152
  }
153
153
 
154
- // Compile parent source.
155
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
154
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
155
+ // matches this harness's real behavior every sibling child template is registered
156
+ // alongside the parent before rendering, so a loop-body cross-template
157
+ // call resolves at render time (#2205).
158
+ const result = compileJSX(source, 'component.tsx', {
159
+ adapter,
160
+ outputIR: true,
161
+ siblingTemplatesRegistered: Boolean(components),
162
+ })
156
163
 
157
164
  const errors = result.errors.filter(e => e.severity === 'error')
158
165
  if (errors.length > 0) {
@@ -495,9 +502,9 @@ function buildRubyProps(
495
502
  for (const param of ir.metadata.propsParams) {
496
503
  if (props && param.name in props) continue
497
504
  if (param.defaultValue) {
498
- const value = jsDefaultLiteral(param.defaultValue)
499
- if (value !== undefined) {
500
- obj[param.name] = value
505
+ const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
506
+ if (result.ok) {
507
+ obj[param.name] = result.value
501
508
  continue
502
509
  }
503
510
  }
@@ -556,133 +563,3 @@ function buildRubyProps(
556
563
  return { obj, needsSearchParams }
557
564
  }
558
565
 
559
- /**
560
- * Best-effort literal evaluation of a prop-destructure default's source
561
- * text (`{ size = 'md' }` → `'md'`), including a `props.x ?? default`
562
- * nullish fallback (handled generically, though destructure defaults
563
- * rarely reference `props`) and delegating to `parseLiteral` for
564
- * everything else. Returns `undefined` for a non-literal (computed)
565
- * default, matching the Perl harnesses' "fall through to undef" behaviour.
566
- */
567
- function jsDefaultLiteral(expr: string): unknown {
568
- const t = expr.trim()
569
- const nullishMatch = t.match(/\?\?\s*(.+)$/)
570
- if (nullishMatch) return jsDefaultLiteral(nullishMatch[1])
571
- if (t.startsWith('props.')) return undefined
572
- const parsed = parseLiteral(t)
573
- return parsed === null && t !== 'null' ? undefined : parsed
574
- }
575
-
576
- /**
577
- * Evaluate a signal initializer expression using provided props.
578
- * Handles: props.initial ?? 0, props.value, literal values.
579
- */
580
- export function evaluateSignalInit(
581
- expr: string,
582
- props?: Record<string, unknown>,
583
- ): unknown {
584
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
585
- if (nullishMatch) {
586
- const propName = nullishMatch[1]
587
- const defaultExpr = nullishMatch[2].trim()
588
- if (props && propName in props) return props[propName]
589
- return parseLiteral(defaultExpr)
590
- }
591
-
592
- const propsMatch = expr.match(/^props\.(\w+)$/)
593
- if (propsMatch) {
594
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
595
- return null
596
- }
597
-
598
- return parseLiteral(expr)
599
- }
600
-
601
- function parseLiteral(expr: string): unknown {
602
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
603
- if (expr === 'true') return true
604
- if (expr === 'false') return false
605
- if (expr === '[]') return []
606
-
607
- {
608
- const t = expr.trim()
609
- if (t.startsWith('[') && t.endsWith(']')) {
610
- const inner = t.slice(1, -1).trim()
611
- if (!inner) return []
612
- const out: unknown[] = []
613
- for (const seg of splitTopLevelCommas(inner)) {
614
- if (!seg.trim()) continue
615
- const parsed = parseLiteral(seg.trim())
616
- if (parsed === null && seg.trim() !== 'null') return null
617
- out.push(parsed)
618
- }
619
- return out
620
- }
621
- }
622
-
623
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
624
- if (stringMatch) return unescapeJsString(stringMatch[2])
625
-
626
- const trimmed = expr.trim()
627
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
628
- const inner = trimmed.slice(1, -1).trim()
629
- if (!inner) return {}
630
- const obj: Record<string, unknown> = {}
631
- for (const pair of splitTopLevelCommas(inner)) {
632
- if (!pair.trim()) continue
633
- const colonIdx = pair.indexOf(':')
634
- if (colonIdx < 0) return null
635
- let key = pair.slice(0, colonIdx).trim()
636
- const val = pair.slice(colonIdx + 1).trim()
637
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
638
- if (keyMatch) key = unescapeJsString(keyMatch[2])
639
- const parsedVal = parseLiteral(val)
640
- if (parsedVal === null && val !== 'null') return null
641
- obj[key] = parsedVal
642
- }
643
- return obj
644
- }
645
- return null
646
- }
647
-
648
- function splitTopLevelCommas(inner: string): string[] {
649
- const segments: string[] = []
650
- let depth = 0
651
- let start = 0
652
- let quote: string | null = null
653
- for (let i = 0; i < inner.length; i++) {
654
- const c = inner[i]
655
- if (quote) {
656
- if (c === quote) {
657
- let backslashes = 0
658
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
659
- if (backslashes % 2 === 0) quote = null
660
- }
661
- continue
662
- }
663
- if (c === '"' || c === "'") {
664
- quote = c
665
- continue
666
- }
667
- if (c === '{' || c === '[') depth++
668
- else if (c === '}' || c === ']') depth--
669
- else if (c === ',' && depth === 0) {
670
- segments.push(inner.slice(start, i))
671
- start = i + 1
672
- }
673
- }
674
- segments.push(inner.slice(start))
675
- return segments
676
- }
677
-
678
- function unescapeJsString(s: string): string {
679
- return s.replace(/\\(.)/g, (_, c) => {
680
- switch (c) {
681
- case 'n': return '\n'
682
- case 'r': return '\r'
683
- case 't': return '\t'
684
- case '0': return '\0'
685
- default: return c
686
- }
687
- })
688
- }