@barefootjs/mojolicious 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.
@@ -5,7 +5,7 @@
5
5
  * Used by adapter-tests conformance runner.
6
6
  */
7
7
 
8
- import { compileJSX, extractSsrDefaults, augmentInheritedPropAccesses, importsSearchParams } from '@barefootjs/jsx'
8
+ import { compileJSX, extractSsrDefaults, augmentInheritedPropAccesses, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
9
9
  import type { ComponentIR } from '@barefootjs/jsx'
10
10
  import { mkdir, rm } from 'node:fs/promises'
11
11
  import { resolve } from 'node:path'
@@ -136,8 +136,15 @@ export async function renderMojoComponent(options: RenderOptions): Promise<strin
136
136
  }
137
137
  }
138
138
 
139
- // Compile parent source
140
- const result = compileJSX(source, 'component.tsx', { adapter, outputIR: true })
139
+ // Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
140
+ // matches this harness's real behavior every sibling child template is registered
141
+ // alongside the parent before rendering, so a loop-body cross-template
142
+ // call resolves at render time (#2205).
143
+ const result = compileJSX(source, 'component.tsx', {
144
+ adapter,
145
+ outputIR: true,
146
+ siblingTemplatesRegistered: Boolean(components),
147
+ })
141
148
 
142
149
  const errors = result.errors.filter(e => e.severity === 'error')
143
150
  if (errors.length > 0) {
@@ -450,9 +457,9 @@ function buildChildDefaultsPerl(ir: ComponentIR): string {
450
457
  for (const param of ir.metadata.propsParams) {
451
458
  declared.add(param.name)
452
459
  if (param.defaultValue) {
453
- const perlValue = jsToPerlValue(param.defaultValue)
454
- if (perlValue !== null) {
455
- entries.push(`${param.name} => ${perlValue}`)
460
+ const result = tryEvaluateSignalInit(param.defaultValue.trim())
461
+ if (result.ok) {
462
+ entries.push(`${param.name} => ${toPerlLiteral(result.value)}`)
456
463
  continue
457
464
  }
458
465
  }
@@ -500,9 +507,9 @@ function buildPerlProps(
500
507
  for (const param of ir.metadata.propsParams) {
501
508
  if (props && param.name in props) continue
502
509
  if (param.defaultValue) {
503
- const perlValue = jsToPerlValue(param.defaultValue)
504
- if (perlValue !== null) {
505
- entries.push(`${param.name} => ${perlValue}`)
510
+ const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
511
+ if (result.ok) {
512
+ entries.push(`${param.name} => ${toPerlLiteral(result.value)}`)
506
513
  continue
507
514
  }
508
515
  }
@@ -676,176 +683,6 @@ function collectPropsObjectAccesses(ir: ComponentIR, propsObj: string): Set<stri
676
683
  return out
677
684
  }
678
685
 
679
- /**
680
- * Evaluate a signal initializer expression using provided props.
681
- * Handles patterns like: props.initial ?? 0, props.value, literal values.
682
- */
683
- export function evaluateSignalInit(
684
- expr: string,
685
- props?: Record<string, unknown>,
686
- ): unknown {
687
- // props.xxx ?? default
688
- const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
689
- if (nullishMatch) {
690
- const propName = nullishMatch[1]
691
- const defaultExpr = nullishMatch[2].trim()
692
- if (props && propName in props) {
693
- return props[propName]
694
- }
695
- return parseLiteral(defaultExpr)
696
- }
697
-
698
- // props.xxx (no default)
699
- const propsMatch = expr.match(/^props\.(\w+)$/)
700
- if (propsMatch) {
701
- if (props && propsMatch[1] in props) {
702
- return props[propsMatch[1]]
703
- }
704
- return null
705
- }
706
-
707
- // Literal value
708
- return parseLiteral(expr)
709
- }
710
-
711
- function parseLiteral(expr: string): unknown {
712
- if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
713
- if (expr === 'true') return true
714
- if (expr === 'false') return false
715
- if (expr === '[]') return []
716
-
717
- // Non-empty array literal (`[{ id: 'a' }, { id: 'b' }]`, `['x', 'y']`).
718
- // Each element is parsed recursively; if any element can't be parsed
719
- // (identifier, call, member access, …) the whole array bails to null so
720
- // the harness falls back to its `undef` behaviour. Mirrors the object-
721
- // literal branch below. Needed so signal initial values that are inline
722
- // object/scalar arrays seed the Mojo SSR stash (e.g. the whole-item loop
723
- // conditional fixture, whose `items` is `[{ id: 'a' }, …]`).
724
- {
725
- const t = expr.trim()
726
- if (t.startsWith('[') && t.endsWith(']')) {
727
- const inner = t.slice(1, -1).trim()
728
- if (!inner) return []
729
- const out: unknown[] = []
730
- for (const seg of splitTopLevelCommas(inner)) {
731
- if (!seg.trim()) continue
732
- const parsed = parseLiteral(seg.trim())
733
- if (parsed === null && seg.trim() !== 'null') return null
734
- out.push(parsed)
735
- }
736
- return out
737
- }
738
- }
739
- // String literal — require matching opener/closer (the previous
740
- // regex `^['"]…['"]$` accepted mixed quotes like `'foo"`) and
741
- // unescape JS-style escape sequences so `'a\\'b'` round-trips as
742
- // `a\'b` instead of leaking the source-level escapes into the
743
- // Perl literal (#1413 review).
744
- const stringMatch = expr.match(/^(['"])(.*)\1$/s)
745
- if (stringMatch) return unescapeJsString(stringMatch[2])
746
- // JS object literal (#1407 follow-up): `{ id: 'a', class: 'on' }`.
747
- // Used for spread-bag signal initial values in the `jsx-spread-*`
748
- // fixture family. Keys may be bare identifiers or string
749
- // literals; values are scalars (string / number / boolean /
750
- // null) or nested object literals via recursive `parseLiteral`.
751
- // Non-empty array values (`[1, 2]`) are NOT supported — only
752
- // the `[]` empty-array literal recognised by the early-return
753
- // above lowers. Trailing commas (`{ id: 'a', }`) are accepted
754
- // by skipping empty segments (#1413 review). Anything the
755
- // recursive call can't handle (identifiers, function calls,
756
- // member access, non-empty arrays) surfaces as null and bubbles
757
- // up so the harness falls back to its existing `undef`
758
- // behaviour.
759
- const trimmed = expr.trim()
760
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
761
- const inner = trimmed.slice(1, -1).trim()
762
- if (!inner) return {}
763
- const obj: Record<string, unknown> = {}
764
- const pairs = splitTopLevelCommas(inner)
765
- for (const pair of pairs) {
766
- // Skip empty segments — typically a trailing comma's tail
767
- // (#1413 review).
768
- if (!pair.trim()) continue
769
- const colonIdx = pair.indexOf(':')
770
- if (colonIdx < 0) return null
771
- let key = pair.slice(0, colonIdx).trim()
772
- const val = pair.slice(colonIdx + 1).trim()
773
- // Strip key quotes if any — require matching open/close
774
- // quote and unescape, same shape as the value-side string
775
- // literal handling above (#1413 review).
776
- const keyMatch = key.match(/^(['"])(.*)\1$/s)
777
- if (keyMatch) key = unescapeJsString(keyMatch[2])
778
- const parsedVal = parseLiteral(val)
779
- if (parsedVal === null && val !== 'null') return null
780
- obj[key] = parsedVal
781
- }
782
- return obj
783
- }
784
- return null
785
- }
786
-
787
- /**
788
- * Split a comma-separated literal body (object-pair list or array element
789
- * list) on top-level commas only — commas nested inside braces, brackets, or
790
- * string literals don't split. Backslash-escaped quotes inside strings are
791
- * honoured (an odd run of backslashes before a quote keeps the string open).
792
- * Shared by the object- and array-literal branches of {@link parseLiteral}.
793
- */
794
- function splitTopLevelCommas(inner: string): string[] {
795
- const segments: string[] = []
796
- let depth = 0
797
- let start = 0
798
- let quote: string | null = null
799
- for (let i = 0; i < inner.length; i++) {
800
- const c = inner[i]
801
- if (quote) {
802
- if (c === quote) {
803
- let backslashes = 0
804
- for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
805
- if (backslashes % 2 === 0) quote = null
806
- }
807
- continue
808
- }
809
- if (c === '"' || c === "'") {
810
- quote = c
811
- continue
812
- }
813
- if (c === '{' || c === '[') depth++
814
- else if (c === '}' || c === ']') depth--
815
- else if (c === ',' && depth === 0) {
816
- segments.push(inner.slice(start, i))
817
- start = i + 1
818
- }
819
- }
820
- segments.push(inner.slice(start))
821
- return segments
822
- }
823
-
824
- /**
825
- * Unescape a JS string-literal body (the content between the
826
- * matching opening and closing quotes, not the quotes themselves).
827
- * Handles the common single-character escapes `\\`, `\'`, `\"`,
828
- * `\n`, `\r`, `\t`, `\0`, and the backslash-anything fallback that
829
- * mirrors JS's "unknown escape is the character itself" semantics.
830
- * Hex / unicode / octal escapes are intentionally out of scope —
831
- * the spread-bag fixture corpus uses ASCII identifiers and short
832
- * literal values, so the harness doesn't need a full JS string
833
- * decoder (#1413 review).
834
- */
835
- function unescapeJsString(s: string): string {
836
- return s.replace(/\\(.)/g, (_, c) => {
837
- switch (c) {
838
- case 'n': return '\n'
839
- case 'r': return '\r'
840
- case 't': return '\t'
841
- case '0': return '\0'
842
- // `\\`, `\'`, `\"`, and any other single-character escape
843
- // collapse to the literal character (matches JS semantics
844
- // for unrecognised escapes).
845
- default: return c
846
- }
847
- })
848
- }
849
686
 
850
687
  /**
851
688
  * Perl single-quoted string escape: `'` AND `\` need escaping.
@@ -891,34 +728,3 @@ function toPerlLiteral(value: unknown): string {
891
728
  return 'undef'
892
729
  }
893
730
 
894
- /**
895
- * Convert a JS literal value to a Perl literal.
896
- * Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default patterns.
897
- */
898
- function jsToPerlValue(jsValue: string): string | null {
899
- const v = jsValue.trim()
900
-
901
- // Number
902
- if (/^-?\d+(\.\d+)?$/.test(v)) return v
903
-
904
- // String literal
905
- if (/^['"].*['"]$/.test(v)) return v
906
-
907
- // Boolean
908
- if (v === 'true') return '1'
909
- if (v === 'false') return '0'
910
-
911
- // Empty array
912
- if (v === '[]') return '[]'
913
-
914
- // props.xxx ?? default — extract the default value
915
- const nullishMatch = v.match(/\?\?\s*(.+)$/)
916
- if (nullishMatch) {
917
- return jsToPerlValue(nullishMatch[1])
918
- }
919
-
920
- // props.xxx (no default) — return undef
921
- if (v.startsWith('props.')) return 'undef'
922
-
923
- return null
924
- }
@@ -1,35 +0,0 @@
1
- import { describe, test, expect } from 'bun:test'
2
- import { evaluateSignalInit } from '../test-render'
3
-
4
- describe('evaluateSignalInit — SSR signal seeding (#1672)', () => {
5
- test('parses an inline object-array initial value', () => {
6
- // The whole-item loop-conditional fixture seeds `items` from an inline
7
- // object array. Without array support this returned null, so `$items` was
8
- // undefined in the Mojo SSR render and the loop rendered empty.
9
- expect(evaluateSignalInit(`[{ id: 'a' }, { id: 'b' }, { id: 'c' }]`)).toEqual([
10
- { id: 'a' },
11
- { id: 'b' },
12
- { id: 'c' },
13
- ])
14
- })
15
-
16
- test('parses scalar and mixed arrays, including nested objects', () => {
17
- expect(evaluateSignalInit(`['x', 'y']`)).toEqual(['x', 'y'])
18
- expect(evaluateSignalInit(`[1, 2, 3]`)).toEqual([1, 2, 3])
19
- expect(evaluateSignalInit(`[{ id: 'a', n: 1, ok: true }]`)).toEqual([
20
- { id: 'a', n: 1, ok: true },
21
- ])
22
- })
23
-
24
- test('still parses scalars, empty array, and props passthrough', () => {
25
- expect(evaluateSignalInit(`'b'`)).toBe('b')
26
- expect(evaluateSignalInit(`5`)).toBe(5)
27
- expect(evaluateSignalInit(`[]`)).toEqual([])
28
- expect(evaluateSignalInit(`props.value`, { value: 42 })).toBe(42)
29
- })
30
-
31
- test('bails to null for arrays with non-literal elements', () => {
32
- // A call / identifier element can't be evaluated at seed time.
33
- expect(evaluateSignalInit(`[foo(), bar]`)).toBeNull()
34
- })
35
- })