@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.
- package/dist/adapter/index.js +79 -6
- package/dist/adapter/lib/static-value.d.ts +17 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +15 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +23 -6
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +79 -6
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +81 -15
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts +0 -5
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +165 -4
- package/src/adapter/lib/static-value.ts +47 -0
- package/src/adapter/mojo-adapter.ts +77 -5
- package/src/adapter/props/prop-classes.ts +27 -6
- package/src/conformance-pins.ts +26 -28
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +16 -210
- package/src/__tests__/evaluate-signal-init.test.ts +0 -35
package/src/test-render.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
454
|
-
if (
|
|
455
|
-
entries.push(`${param.name} => ${
|
|
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
|
|
504
|
-
if (
|
|
505
|
-
entries.push(`${param.name} => ${
|
|
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
|
-
})
|