@barefootjs/xslate 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.
@@ -9,17 +9,17 @@
9
9
  import type { ConformancePins } from '@barefootjs/jsx'
10
10
 
11
11
  export const conformancePins: ConformancePins = {
12
- // Sibling-imported child component in a loop body: emits a
13
- // cross-template call needing separate registration. BF103 makes
14
- // the requirement loud (same as mojo).
15
- 'static-array-children': [{ code: 'BF103', severity: 'error' }],
16
- // TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
17
- // call it inside a keyed `.map`. With the standalone-filter fix in
18
- // place these reach the SAME BF103 (imported child in `.map`) as
19
- // mojo NOT BF101 confirming the `.filter(...)` chain itself now
20
- // lowers and the only remaining gate is the imported-child one.
21
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
22
- 'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
12
+ // `todo-app` / `todo-app-ssr` no longer pinned (#2205) the conformance
13
+ // harness now passes `siblingTemplatesRegistered: true` for fixtures with
14
+ // sibling `components`, matching `bf build`'s real semantics, so the
15
+ // BF103 loop-body cross-template check no longer fires spuriously. (Both
16
+ // fixtures are still skipped on this adapter via `render-divergences.ts`
17
+ // #2209 for an unrelated signal-seeding gap.)
18
+ // `static-array-children` no longer pinned (#2208) `items`'s
19
+ // array-literal initializer is now recognized as fully-static
20
+ // (`resolveStaticLoopSource`) and inlined as a native Kolon array/hash
21
+ // literal in the `for EXPR -> $item` header, the same way a module-scope
22
+ // const's value is already seeded.
23
23
  // `([emoji, users]) => ...` / `([id, t]) => ...` are plain array-index
24
24
  // (tuple) destructures, no rest — #2087 Phase B's `segments`-walking
25
25
  // accessor lowers both to `$__bf_item[0]` / `$__bf_item[1]` `: my` locals
@@ -32,12 +32,11 @@ export const conformancePins: ConformancePins = {
32
32
  // simply unreachable before because BF104 refused the destructure shape
33
33
  // first.
34
34
  'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
35
- // Both BF103 (sibling-imported `<Tag>` child component) and the BF101
36
- // above fire; BF104 no longer does (see above).
37
- 'static-array-from-props-with-component': [
38
- { code: 'BF103', severity: 'error' },
39
- { code: 'BF101', severity: 'error' },
40
- ],
35
+ // The BF101 above fires; BF104 no longer does (see above), and BF103
36
+ // (sibling-imported `<Tag>` child component in the loop body) no longer
37
+ // does either now that the conformance harness passes
38
+ // `siblingTemplatesRegistered: true` (#2205).
39
+ 'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
41
40
  // #1310 / #2087: rest destructure in .map() callback. All four shapes now
42
41
  // lower via #2087 Phase B's `segments`-walking accessor:
43
42
  // - object-rest read via member access (`rest-destructure-object-in-map`):
@@ -104,15 +103,14 @@ export const conformancePins: ConformancePins = {
104
103
  // `find_last_index` via the same Kolon-lambda mechanism as `.filter` /
105
104
  // `.every` / `.some`, so they render. Only the NESTED-in-a-predicate form
106
105
  // above is refused (#2038).
107
- // #2073 follow-up: a function-reference `.map(format)` callback has no
108
- // arrow body to serialize not a CALLBACK_METHODS shape — so the
109
- // UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
110
- // a broken template.
111
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
112
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
113
- // deliberate raw-HTML (unescaped) output affordance in the target
114
- // template language. No lowering exists yet, so the compiler refuses
115
- // the shape loudly instead of emitting entity-escaped markup that
116
- // silently renders tags as text.
117
- 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
106
+ // `array-map-function-reference` no longer pinned — a bare-identifier
107
+ // `.map(format)` callback now resolves one hop to its declaration
108
+ // (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
109
+ // #2090 established for `.sort(fnref)`.
110
+ // `dangerous-inner-html` no longer pinned a compile-time string-literal
111
+ // `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
112
+ // the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
113
+ // A dynamic/signal-derived value still refuses with BF101 see the
114
+ // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
115
+ 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
118
116
  }
@@ -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
+ }
@@ -17,7 +17,7 @@
17
17
  * than the Mojo harness's literal `test_<sN>`.
18
18
  */
19
19
 
20
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
20
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
21
21
  import type { ComponentIR } from '@barefootjs/jsx'
22
22
  import { mkdir, rm } from 'node:fs/promises'
23
23
  import { resolve } from 'node:path'
@@ -135,8 +135,15 @@ export async function renderXslateComponent(options: RenderOptions): Promise<str
135
135
  }
136
136
  }
137
137
 
138
- // Compile parent source.
139
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
138
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
139
+ // matches this harness's real behavior every sibling child template is registered
140
+ // alongside the parent before rendering, so a loop-body cross-template
141
+ // call resolves at render time (#2205).
142
+ const result = compileJSX(source, 'component.tsx', {
143
+ adapter,
144
+ outputIR: true,
145
+ siblingTemplatesRegistered: Boolean(components),
146
+ })
140
147
 
141
148
  const errors = result.errors.filter(e => e.severity === 'error')
142
149
  if (errors.length > 0) {
@@ -467,9 +474,9 @@ function buildPerlProps(
467
474
  for (const param of ir.metadata.propsParams) {
468
475
  if (props && param.name in props) continue
469
476
  if (param.defaultValue) {
470
- const perlValue = jsToPerlValue(param.defaultValue)
471
- if (perlValue !== null) {
472
- entries.push(`${param.name} => ${perlValue}`)
477
+ const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
478
+ if (result.ok) {
479
+ entries.push(`${param.name} => ${toPerlLiteral(result.value)}`)
473
480
  continue
474
481
  }
475
482
  }
@@ -547,119 +554,6 @@ function buildPerlProps(
547
554
  return `{${entries.join(', ')}}`
548
555
  }
549
556
 
550
- /**
551
- * Evaluate a signal initializer expression using provided props.
552
- * Handles: props.initial ?? 0, props.value, literal values.
553
- */
554
- export function evaluateSignalInit(
555
- expr: string,
556
- props?: Record<string, unknown>,
557
- ): unknown {
558
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
559
- if (nullishMatch) {
560
- const propName = nullishMatch[1]
561
- const defaultExpr = nullishMatch[2].trim()
562
- if (props && propName in props) return props[propName]
563
- return parseLiteral(defaultExpr)
564
- }
565
-
566
- const propsMatch = expr.match(/^props\.(\w+)$/)
567
- if (propsMatch) {
568
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
569
- return null
570
- }
571
-
572
- return parseLiteral(expr)
573
- }
574
-
575
- function parseLiteral(expr: string): unknown {
576
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
577
- if (expr === 'true') return true
578
- if (expr === 'false') return false
579
- if (expr === '[]') return []
580
-
581
- {
582
- const t = expr.trim()
583
- if (t.startsWith('[') && t.endsWith(']')) {
584
- const inner = t.slice(1, -1).trim()
585
- if (!inner) return []
586
- const out: unknown[] = []
587
- for (const seg of splitTopLevelCommas(inner)) {
588
- if (!seg.trim()) continue
589
- const parsed = parseLiteral(seg.trim())
590
- if (parsed === null && seg.trim() !== 'null') return null
591
- out.push(parsed)
592
- }
593
- return out
594
- }
595
- }
596
-
597
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
598
- if (stringMatch) return unescapeJsString(stringMatch[2])
599
-
600
- const trimmed = expr.trim()
601
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
602
- const inner = trimmed.slice(1, -1).trim()
603
- if (!inner) return {}
604
- const obj: Record<string, unknown> = {}
605
- for (const pair of splitTopLevelCommas(inner)) {
606
- if (!pair.trim()) continue
607
- const colonIdx = pair.indexOf(':')
608
- if (colonIdx < 0) return null
609
- let key = pair.slice(0, colonIdx).trim()
610
- const val = pair.slice(colonIdx + 1).trim()
611
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
612
- if (keyMatch) key = unescapeJsString(keyMatch[2])
613
- const parsedVal = parseLiteral(val)
614
- if (parsedVal === null && val !== 'null') return null
615
- obj[key] = parsedVal
616
- }
617
- return obj
618
- }
619
- return null
620
- }
621
-
622
- function splitTopLevelCommas(inner: string): string[] {
623
- const segments: string[] = []
624
- let depth = 0
625
- let start = 0
626
- let quote: string | null = null
627
- for (let i = 0; i < inner.length; i++) {
628
- const c = inner[i]
629
- if (quote) {
630
- if (c === quote) {
631
- let backslashes = 0
632
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
633
- if (backslashes % 2 === 0) quote = null
634
- }
635
- continue
636
- }
637
- if (c === '"' || c === "'") {
638
- quote = c
639
- continue
640
- }
641
- if (c === '{' || c === '[') depth++
642
- else if (c === '}' || c === ']') depth--
643
- else if (c === ',' && depth === 0) {
644
- segments.push(inner.slice(start, i))
645
- start = i + 1
646
- }
647
- }
648
- segments.push(inner.slice(start))
649
- return segments
650
- }
651
-
652
- function unescapeJsString(s: string): string {
653
- return s.replace(/\\(.)/g, (_, c) => {
654
- switch (c) {
655
- case 'n': return '\n'
656
- case 'r': return '\r'
657
- case 't': return '\t'
658
- case '0': return '\0'
659
- default: return c
660
- }
661
- })
662
- }
663
557
 
664
558
  /** Perl single-quoted string escape: `'` AND `\` need escaping. */
665
559
  function perlSingleQuote(s: string): string {
@@ -685,23 +579,3 @@ function toPerlLiteral(value: unknown): string {
685
579
  return 'undef'
686
580
  }
687
581
 
688
- /**
689
- * Convert a JS literal value to a Perl literal.
690
- * Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default.
691
- */
692
- function jsToPerlValue(jsValue: string): string | null {
693
- const v = jsValue.trim()
694
-
695
- if (/^-?\d+(\.\d+)?$/.test(v)) return v
696
- if (/^['"].*['"]$/.test(v)) return v
697
- if (v === 'true') return '1'
698
- if (v === 'false') return '0'
699
- if (v === '[]') return '[]'
700
-
701
- const nullishMatch = v.match(/\?\?\s*(.+)$/)
702
- if (nullishMatch) return jsToPerlValue(nullishMatch[1])
703
-
704
- if (v.startsWith('props.')) return 'undef'
705
-
706
- return null
707
- }