@barefootjs/rust 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.
@@ -12,15 +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 xslate).
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 (imported child in
21
- // `.map`) as xslate.
22
- 'todo-app': [{ code: 'BF103', severity: 'error' }],
23
- '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 MiniJinja
24
+ // list/dict literal in the `{% for %}` header, the same way a
25
+ // module-scope const's value is already seeded.
24
26
  // The `([emoji, users]) => ...` / `([id, t]) => ...` params in these two
25
27
  // fixtures no longer trip BF104 — the destructure itself now lowers
26
28
  // cleanly to a native `{% set %}` accessor (#2087 Phase B). But both
@@ -44,10 +46,10 @@ export const conformancePins: ConformancePins = {
44
46
  'static-array-from-props': [
45
47
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
46
48
  ],
47
- // Both BF103 (imported child) and BF101 (unresolvable computed loop
48
- // array, see above) fire.
49
+ // BF101 (unresolvable computed loop array, see above) fires; BF103
50
+ // (imported child in the loop body) no longer does now that the
51
+ // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
49
52
  'static-array-from-props-with-component': [
50
- { code: 'BF103', severity: 'error' },
51
53
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
52
54
  ],
53
55
  // Rest-destructure `.map()` callbacks (#2087 Phase B): every shape now
@@ -91,22 +93,14 @@ export const conformancePins: ConformancePins = {
91
93
  // / etc. via the same evaluator-JSON mechanism as `.filter` / `.every` /
92
94
  // `.some`, so they render. Only the NESTED-in-a-predicate form above is
93
95
  // refused (#2038).
94
- // #2073 follow-up (same as xslate): a function-reference `.map(format)`
95
- // callback has no arrow body to serialize — not a CALLBACK_METHODS shape
96
- // (`asCallbackMethodCall` requires an arrow argument) so the shared
97
- // `isSupported`'s `UNSUPPORTED_METHODS` gate refuses it with the generic
98
- // "Expression not supported" BF101 rather than emitting a broken
99
- // template.
100
- 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
101
- // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
102
- // deliberate raw-HTML (unescaped) output affordance in the target
103
- // template language. No lowering exists yet, so the compiler refuses
104
- // the shape loudly instead of emitting entity-escaped markup that
105
- // silently renders tags as text.
106
- 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
107
- // Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
108
- // only first-occurrence `.replace` is wired to the runtime helpers.
109
- // Refused with BF101 rather than reusing the first-only lowering,
110
- // which would silently change semantics.
111
- 'string-replaceall': [{ 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' }],
112
106
  }
@@ -17,20 +17,8 @@
17
17
  import type { RenderDivergences } from '@barefootjs/jsx'
18
18
 
19
19
  export const renderDivergences: RenderDivergences = {
20
- 'html-entity-text':
21
- '`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
22
- 'math-methods':
23
- 'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
24
- 'static-attr-escape':
25
- 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
26
- 'object-entries-map':
27
- '`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
28
- 'nested-loop-outer-binding':
29
- 'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
30
- 'jsx-element-prop':
31
- 'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
32
- 'string-slice':
33
- '`.slice()` on a STRING renders empty (array-slice helper misfires on strings)',
34
- 'string-trim-sided':
35
- '`.trimStart()` / `.trimEnd()` render empty (no lowering)',
20
+ // `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
21
+ // `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
22
+ // instead of a fixed regex-shape catalogue) now correctly seeds `todos`
23
+ // from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
36
24
  }
@@ -28,7 +28,7 @@
28
28
  * that closes that gap.
29
29
  */
30
30
 
31
- import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
31
+ import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
32
32
  import type { ComponentIR } from '@barefootjs/jsx'
33
33
  import { mkdir, rm } from 'node:fs/promises'
34
34
  import { resolve } from 'node:path'
@@ -170,8 +170,15 @@ export async function renderMinijinjaComponent(options: RenderOptions): Promise<
170
170
  }
171
171
  }
172
172
 
173
- // Compile parent source.
174
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
173
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
174
+ // matches this harness's real behavior every sibling child template is registered
175
+ // alongside the parent before rendering, so a loop-body cross-template
176
+ // call resolves at render time (#2205).
177
+ const result = compileJSX(source, 'component.tsx', {
178
+ adapter,
179
+ outputIR: true,
180
+ siblingTemplatesRegistered: Boolean(components),
181
+ })
175
182
 
176
183
  const errors = result.errors.filter(e => e.severity === 'error')
177
184
  if (errors.length > 0) {
@@ -441,7 +448,7 @@ function buildVars(
441
448
  for (const param of ir.metadata.propsParams) {
442
449
  if (props && param.name in props) continue
443
450
  if (param.defaultValue) {
444
- const value = jsDefaultToVarValue(param.defaultValue)
451
+ const value = evaluateSignalInit(param.defaultValue.trim(), props)
445
452
  if (value !== null) {
446
453
  vars[param.name] = value
447
454
  continue
@@ -510,146 +517,6 @@ function buildVars(
510
517
  return vars
511
518
  }
512
519
 
513
- /**
514
- * Convert a destructure-default's JS source text (`{ size = 'md' }`'s
515
- * `'md'`) to a real JS value. Near-verbatim port of `buildPythonProps`'s
516
- * `jsToPyValue` helper — which returned Python SOURCE text (safe to reuse
517
- * verbatim for a string/numeric literal, since JS and Python share that
518
- * literal grammar) — ported to resolve directly to the JS runtime value
519
- * instead, via the shared `parseLiteral` for the literal shapes both
520
- * versions handle identically. The match ORDER is preserved from
521
- * `jsToPyValue`: numeric/string/bool/`[]` are checked BEFORE the `??`
522
- * regex, so a string literal containing a literal `??` substring (e.g.
523
- * `'a??b'`) is caught by the string-literal branch first, not
524
- * mis-parsed as a nullish-coalescing default.
525
- */
526
- function jsDefaultToVarValue(jsValue: string): unknown {
527
- const v = jsValue.trim()
528
- if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v)
529
- const strMatch = v.match(/^(['"])(.*)\1$/s)
530
- if (strMatch) return unescapeJsString(strMatch[2])
531
- if (v === 'true') return true
532
- if (v === 'false') return false
533
- if (v === '[]') return []
534
- const nullishMatch = v.match(/\?\?\s*(.+)$/)
535
- if (nullishMatch) return jsDefaultToVarValue(nullishMatch[1])
536
- if (v.startsWith('props.')) return null
537
- return null
538
- }
539
-
540
- /**
541
- * Evaluate a signal initializer expression using provided props.
542
- * Handles: props.initial ?? 0, props.value, literal values.
543
- */
544
- export function evaluateSignalInit(
545
- expr: string,
546
- props?: Record<string, unknown>,
547
- ): unknown {
548
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
549
- if (nullishMatch) {
550
- const propName = nullishMatch[1]
551
- const defaultExpr = nullishMatch[2].trim()
552
- if (props && propName in props) return props[propName]
553
- return parseLiteral(defaultExpr)
554
- }
555
-
556
- const propsMatch = expr.match(/^props\.(\w+)$/)
557
- if (propsMatch) {
558
- if (props && propsMatch[1] in props) return props[propsMatch[1]]
559
- return null
560
- }
561
-
562
- return parseLiteral(expr)
563
- }
564
-
565
- function parseLiteral(expr: string): unknown {
566
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
567
- if (expr === 'true') return true
568
- if (expr === 'false') return false
569
- if (expr === '[]') return []
570
-
571
- {
572
- const t = expr.trim()
573
- if (t.startsWith('[') && t.endsWith(']')) {
574
- const inner = t.slice(1, -1).trim()
575
- if (!inner) return []
576
- const out: unknown[] = []
577
- for (const seg of splitTopLevelCommas(inner)) {
578
- if (!seg.trim()) continue
579
- const parsed = parseLiteral(seg.trim())
580
- if (parsed === null && seg.trim() !== 'null') return null
581
- out.push(parsed)
582
- }
583
- return out
584
- }
585
- }
586
-
587
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
588
- if (stringMatch) return unescapeJsString(stringMatch[2])
589
-
590
- const trimmed = expr.trim()
591
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
592
- const inner = trimmed.slice(1, -1).trim()
593
- if (!inner) return {}
594
- const obj: Record<string, unknown> = {}
595
- for (const pair of splitTopLevelCommas(inner)) {
596
- if (!pair.trim()) continue
597
- const colonIdx = pair.indexOf(':')
598
- if (colonIdx < 0) return null
599
- let key = pair.slice(0, colonIdx).trim()
600
- const val = pair.slice(colonIdx + 1).trim()
601
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
602
- if (keyMatch) key = unescapeJsString(keyMatch[2])
603
- const parsedVal = parseLiteral(val)
604
- if (parsedVal === null && val !== 'null') return null
605
- obj[key] = parsedVal
606
- }
607
- return obj
608
- }
609
- return null
610
- }
611
-
612
- function splitTopLevelCommas(inner: string): string[] {
613
- const segments: string[] = []
614
- let depth = 0
615
- let start = 0
616
- let quote: string | null = null
617
- for (let i = 0; i < inner.length; i++) {
618
- const c = inner[i]
619
- if (quote) {
620
- if (c === quote) {
621
- let backslashes = 0
622
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
623
- if (backslashes % 2 === 0) quote = null
624
- }
625
- continue
626
- }
627
- if (c === '"' || c === "'") {
628
- quote = c
629
- continue
630
- }
631
- if (c === '{' || c === '[') depth++
632
- else if (c === '}' || c === ']') depth--
633
- else if (c === ',' && depth === 0) {
634
- segments.push(inner.slice(start, i))
635
- start = i + 1
636
- }
637
- }
638
- segments.push(inner.slice(start))
639
- return segments
640
- }
641
-
642
- function unescapeJsString(s: string): string {
643
- return s.replace(/\\(.)/g, (_, c) => {
644
- switch (c) {
645
- case 'n': return '\n'
646
- case 'r': return '\r'
647
- case 't': return '\t'
648
- case '0': return '\0'
649
- default: return c
650
- }
651
- })
652
- }
653
520
 
654
521
  /**
655
522
  * Recursively replace JS's non-finite numbers (`NaN`, `Infinity`,