@barefootjs/mojolicious 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.
- package/dist/adapter/index.js +80 -7
- 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 +27 -11
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +80 -7
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +82 -16
- 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 +187 -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 +32 -12
- package/src/conformance-pins.ts +26 -28
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +25 -211
- package/src/__tests__/evaluate-signal-init.test.ts +0 -35
|
@@ -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
|
+
}
|
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
|
}
|
|
@@ -576,7 +583,15 @@ function buildPerlProps(
|
|
|
576
583
|
if (props) {
|
|
577
584
|
for (const [key, value] of Object.entries(props)) {
|
|
578
585
|
if (routedKeys.has(key)) continue
|
|
579
|
-
if (
|
|
586
|
+
if (value === null) {
|
|
587
|
+
// An explicit-null prop must still DECLARE the template var —
|
|
588
|
+
// `typeof null === 'object'` fails every branch below, so the key
|
|
589
|
+
// emitted nothing and `Mojo::Template`'s `vars => 1` never ran
|
|
590
|
+
// `my $user`, tripping Perl's strict-mode "Global symbol requires
|
|
591
|
+
// explicit package name" before the `//` fallback could apply
|
|
592
|
+
// (data-point conformance, optional-chaining-prop:null-user).
|
|
593
|
+
entries.push(`${key} => undef`)
|
|
594
|
+
} else if (typeof value === 'string') {
|
|
580
595
|
entries.push(`${key} => '${value.replace(/'/g, "\\'")}'`)
|
|
581
596
|
} else if (typeof value === 'number') {
|
|
582
597
|
entries.push(`${key} => ${value}`)
|
|
@@ -676,176 +691,6 @@ function collectPropsObjectAccesses(ir: ComponentIR, propsObj: string): Set<stri
|
|
|
676
691
|
return out
|
|
677
692
|
}
|
|
678
693
|
|
|
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
694
|
|
|
850
695
|
/**
|
|
851
696
|
* Perl single-quoted string escape: `'` AND `\` need escaping.
|
|
@@ -891,34 +736,3 @@ function toPerlLiteral(value: unknown): string {
|
|
|
891
736
|
return 'undef'
|
|
892
737
|
}
|
|
893
738
|
|
|
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
|
-
})
|