@barefootjs/mojolicious 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.
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +2 -2
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +151 -34
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- 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 +23 -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 +151 -34
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +154 -56
- 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 +186 -5
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +18 -3
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/lib/static-value.ts +47 -0
- package/src/adapter/mojo-adapter.ts +148 -16
- package/src/adapter/props/prop-classes.ts +27 -6
- package/src/conformance-pins.ts +26 -33
- package/src/render-divergences.ts +4 -20
- package/src/test-render.ts +21 -210
- package/src/__tests__/evaluate-signal-init.test.ts +0 -35
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* adapter's `props/prop-types.ts`. No adapter instance state.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { collectLoopBoundNames, type ComponentIR } from '@barefootjs/jsx'
|
|
11
11
|
import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -67,11 +67,28 @@ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* String-typed signals
|
|
71
|
-
* to `eq`/`ne` (#1672)
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* String-typed signals, props, and same-file local consts, so equality
|
|
71
|
+
* comparisons against them lower to `eq`/`ne` (#1672) and `+` concatenation
|
|
72
|
+
* against them lowers to Perl's `.` instead of numeric `+` (#2163, #2212 —
|
|
73
|
+
* `isStringConcatBinary`/`isStringTypedOperand` in `@barefootjs/jsx`, which
|
|
74
|
+
* now also recognizes a bare identifier operand, not just a prop/getter/
|
|
75
|
+
* literal). A signal is string-typed when its inferred type is `string`
|
|
76
|
+
* (the analyzer infers this from a string-literal initial value) or,
|
|
77
|
+
* defensively, when its initial value is a bare string literal; a prop or
|
|
78
|
+
* local const when its annotated (or inferred) type is `string`.
|
|
79
|
+
*
|
|
80
|
+
* Excludes any name bound as a `.map()`/`.filter()` loop callback's item
|
|
81
|
+
* or index parameter ANYWHERE in the component (Fable review, #2212): the
|
|
82
|
+
* lookup below is a flat, scope-blind `Set<string>` with no notion of a
|
|
83
|
+
* loop param shadowing an outer string-typed binding of the same name
|
|
84
|
+
* (`items.map((name) => 1 + name)` inside a component that also has a
|
|
85
|
+
* string `name` prop) — left unguarded, that shadowed `name` would be
|
|
86
|
+
* misdetected as string-typed and `1 + name` would silently lower to `.`
|
|
87
|
+
* instead of staying numeric `+`. Subtracting loop-bound names is coarse
|
|
88
|
+
* (it also suppresses a genuinely non-shadowed same-named string
|
|
89
|
+
* elsewhere in the component) but safe: the suppressed case just falls
|
|
90
|
+
* back to today's numeric `+` — the same, already-accepted residual as an
|
|
91
|
+
* unresolvable operand — never silently-wrong output.
|
|
75
92
|
*/
|
|
76
93
|
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
77
94
|
const names = new Set<string>()
|
|
@@ -83,5 +100,9 @@ export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
|
83
100
|
for (const p of ir.metadata.propsParams) {
|
|
84
101
|
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
85
102
|
}
|
|
103
|
+
for (const c of ir.metadata.localConstants) {
|
|
104
|
+
if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value)) names.add(c.name)
|
|
105
|
+
}
|
|
106
|
+
for (const bound of collectLoopBoundNames(ir)) names.delete(bound)
|
|
86
107
|
return names
|
|
87
108
|
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
10
10
|
|
|
11
11
|
export const conformancePins: ConformancePins = {
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
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 Perl
|
|
21
|
+
// arrayref/hashref literal in the loop-bound expression, the same way a
|
|
22
|
+
// module-scope 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`
|
|
@@ -32,12 +32,11 @@ export const conformancePins: ConformancePins = {
|
|
|
32
32
|
// check in `renderLoop`. This was always true; it was simply
|
|
33
33
|
// unreachable before because BF104 refused the destructure shape first.
|
|
34
34
|
'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
41
|
// now lower via #2087 Phase B's `segments`-walking accessor:
|
|
43
42
|
// - object-rest read via member access (`rest-destructure-object-in-map`):
|
|
@@ -130,20 +129,14 @@ export const conformancePins: ConformancePins = {
|
|
|
130
129
|
// runtime `bf->find` / `find_index` / `find_last` / `find_last_index` helpers
|
|
131
130
|
// (per-element coderef predicate), matching Xslate. `.join` was never
|
|
132
131
|
// pinned (handled by `renderArrayMethod`'s `case 'join'`).
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
144
|
-
// Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
|
|
145
|
-
// only first-occurrence `.replace` is wired to the runtime helpers.
|
|
146
|
-
// Refused with BF101 rather than reusing the first-only lowering,
|
|
147
|
-
// which would silently change semantics.
|
|
148
|
-
'string-replaceall': [{ code: 'BF101', severity: 'error' }],
|
|
132
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
133
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
134
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
135
|
+
// #2090 established for `.sort(fnref)`.
|
|
136
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
137
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
138
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
139
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
140
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
141
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
149
142
|
}
|
|
@@ -15,24 +15,8 @@
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
17
|
export const renderDivergences: RenderDivergences = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'html-entity-text':
|
|
23
|
-
'`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
|
|
24
|
-
'math-methods':
|
|
25
|
-
'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
|
|
26
|
-
'static-attr-escape':
|
|
27
|
-
'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
28
|
-
'object-entries-map':
|
|
29
|
-
'`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
|
|
30
|
-
'nested-loop-outer-binding':
|
|
31
|
-
'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
|
|
32
|
-
'jsx-element-prop':
|
|
33
|
-
'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
|
|
34
|
-
'string-slice':
|
|
35
|
-
'`.slice()` on a STRING misfires through the array slice helper',
|
|
36
|
-
'string-trim-sided':
|
|
37
|
-
'`.trimStart()` / `.trimEnd()` render empty (no lowering)',
|
|
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 }))`.
|
|
38
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) {
|
|
@@ -240,6 +247,11 @@ export async function renderMojoComponent(options: RenderOptions): Promise<strin
|
|
|
240
247
|
use strict;
|
|
241
248
|
use warnings;
|
|
242
249
|
use utf8;
|
|
250
|
+
# The template is read through :utf8, so $output holds decoded wide
|
|
251
|
+
# characters; without a matching layer on STDOUT Perl emits them as
|
|
252
|
+
# latin-1 bytes and the harness reads U+FFFD (©/¥ mangling). A real
|
|
253
|
+
# Mojolicious response encodes on the way out — this is harness-only.
|
|
254
|
+
binmode STDOUT, ':encoding(UTF-8)';
|
|
243
255
|
|
|
244
256
|
use lib '${LIB_DIR}', '${PERL_CORE_LIB_DIR}';
|
|
245
257
|
use Mojolicious;
|
|
@@ -445,9 +457,9 @@ function buildChildDefaultsPerl(ir: ComponentIR): string {
|
|
|
445
457
|
for (const param of ir.metadata.propsParams) {
|
|
446
458
|
declared.add(param.name)
|
|
447
459
|
if (param.defaultValue) {
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
-
entries.push(`${param.name} => ${
|
|
460
|
+
const result = tryEvaluateSignalInit(param.defaultValue.trim())
|
|
461
|
+
if (result.ok) {
|
|
462
|
+
entries.push(`${param.name} => ${toPerlLiteral(result.value)}`)
|
|
451
463
|
continue
|
|
452
464
|
}
|
|
453
465
|
}
|
|
@@ -495,9 +507,9 @@ function buildPerlProps(
|
|
|
495
507
|
for (const param of ir.metadata.propsParams) {
|
|
496
508
|
if (props && param.name in props) continue
|
|
497
509
|
if (param.defaultValue) {
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
entries.push(`${param.name} => ${
|
|
510
|
+
const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
|
|
511
|
+
if (result.ok) {
|
|
512
|
+
entries.push(`${param.name} => ${toPerlLiteral(result.value)}`)
|
|
501
513
|
continue
|
|
502
514
|
}
|
|
503
515
|
}
|
|
@@ -671,176 +683,6 @@ function collectPropsObjectAccesses(ir: ComponentIR, propsObj: string): Set<stri
|
|
|
671
683
|
return out
|
|
672
684
|
}
|
|
673
685
|
|
|
674
|
-
/**
|
|
675
|
-
* Evaluate a signal initializer expression using provided props.
|
|
676
|
-
* Handles patterns like: props.initial ?? 0, props.value, literal values.
|
|
677
|
-
*/
|
|
678
|
-
export function evaluateSignalInit(
|
|
679
|
-
expr: string,
|
|
680
|
-
props?: Record<string, unknown>,
|
|
681
|
-
): unknown {
|
|
682
|
-
// props.xxx ?? default
|
|
683
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
684
|
-
if (nullishMatch) {
|
|
685
|
-
const propName = nullishMatch[1]
|
|
686
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
687
|
-
if (props && propName in props) {
|
|
688
|
-
return props[propName]
|
|
689
|
-
}
|
|
690
|
-
return parseLiteral(defaultExpr)
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// props.xxx (no default)
|
|
694
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
695
|
-
if (propsMatch) {
|
|
696
|
-
if (props && propsMatch[1] in props) {
|
|
697
|
-
return props[propsMatch[1]]
|
|
698
|
-
}
|
|
699
|
-
return null
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Literal value
|
|
703
|
-
return parseLiteral(expr)
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function parseLiteral(expr: string): unknown {
|
|
707
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
708
|
-
if (expr === 'true') return true
|
|
709
|
-
if (expr === 'false') return false
|
|
710
|
-
if (expr === '[]') return []
|
|
711
|
-
|
|
712
|
-
// Non-empty array literal (`[{ id: 'a' }, { id: 'b' }]`, `['x', 'y']`).
|
|
713
|
-
// Each element is parsed recursively; if any element can't be parsed
|
|
714
|
-
// (identifier, call, member access, …) the whole array bails to null so
|
|
715
|
-
// the harness falls back to its `undef` behaviour. Mirrors the object-
|
|
716
|
-
// literal branch below. Needed so signal initial values that are inline
|
|
717
|
-
// object/scalar arrays seed the Mojo SSR stash (e.g. the whole-item loop
|
|
718
|
-
// conditional fixture, whose `items` is `[{ id: 'a' }, …]`).
|
|
719
|
-
{
|
|
720
|
-
const t = expr.trim()
|
|
721
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
722
|
-
const inner = t.slice(1, -1).trim()
|
|
723
|
-
if (!inner) return []
|
|
724
|
-
const out: unknown[] = []
|
|
725
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
726
|
-
if (!seg.trim()) continue
|
|
727
|
-
const parsed = parseLiteral(seg.trim())
|
|
728
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
729
|
-
out.push(parsed)
|
|
730
|
-
}
|
|
731
|
-
return out
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
// String literal — require matching opener/closer (the previous
|
|
735
|
-
// regex `^['"]…['"]$` accepted mixed quotes like `'foo"`) and
|
|
736
|
-
// unescape JS-style escape sequences so `'a\\'b'` round-trips as
|
|
737
|
-
// `a\'b` instead of leaking the source-level escapes into the
|
|
738
|
-
// Perl literal (#1413 review).
|
|
739
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
740
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
741
|
-
// JS object literal (#1407 follow-up): `{ id: 'a', class: 'on' }`.
|
|
742
|
-
// Used for spread-bag signal initial values in the `jsx-spread-*`
|
|
743
|
-
// fixture family. Keys may be bare identifiers or string
|
|
744
|
-
// literals; values are scalars (string / number / boolean /
|
|
745
|
-
// null) or nested object literals via recursive `parseLiteral`.
|
|
746
|
-
// Non-empty array values (`[1, 2]`) are NOT supported — only
|
|
747
|
-
// the `[]` empty-array literal recognised by the early-return
|
|
748
|
-
// above lowers. Trailing commas (`{ id: 'a', }`) are accepted
|
|
749
|
-
// by skipping empty segments (#1413 review). Anything the
|
|
750
|
-
// recursive call can't handle (identifiers, function calls,
|
|
751
|
-
// member access, non-empty arrays) surfaces as null and bubbles
|
|
752
|
-
// up so the harness falls back to its existing `undef`
|
|
753
|
-
// behaviour.
|
|
754
|
-
const trimmed = expr.trim()
|
|
755
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
756
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
757
|
-
if (!inner) return {}
|
|
758
|
-
const obj: Record<string, unknown> = {}
|
|
759
|
-
const pairs = splitTopLevelCommas(inner)
|
|
760
|
-
for (const pair of pairs) {
|
|
761
|
-
// Skip empty segments — typically a trailing comma's tail
|
|
762
|
-
// (#1413 review).
|
|
763
|
-
if (!pair.trim()) continue
|
|
764
|
-
const colonIdx = pair.indexOf(':')
|
|
765
|
-
if (colonIdx < 0) return null
|
|
766
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
767
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
768
|
-
// Strip key quotes if any — require matching open/close
|
|
769
|
-
// quote and unescape, same shape as the value-side string
|
|
770
|
-
// literal handling above (#1413 review).
|
|
771
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
772
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
773
|
-
const parsedVal = parseLiteral(val)
|
|
774
|
-
if (parsedVal === null && val !== 'null') return null
|
|
775
|
-
obj[key] = parsedVal
|
|
776
|
-
}
|
|
777
|
-
return obj
|
|
778
|
-
}
|
|
779
|
-
return null
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Split a comma-separated literal body (object-pair list or array element
|
|
784
|
-
* list) on top-level commas only — commas nested inside braces, brackets, or
|
|
785
|
-
* string literals don't split. Backslash-escaped quotes inside strings are
|
|
786
|
-
* honoured (an odd run of backslashes before a quote keeps the string open).
|
|
787
|
-
* Shared by the object- and array-literal branches of {@link parseLiteral}.
|
|
788
|
-
*/
|
|
789
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
790
|
-
const segments: string[] = []
|
|
791
|
-
let depth = 0
|
|
792
|
-
let start = 0
|
|
793
|
-
let quote: string | null = null
|
|
794
|
-
for (let i = 0; i < inner.length; i++) {
|
|
795
|
-
const c = inner[i]
|
|
796
|
-
if (quote) {
|
|
797
|
-
if (c === quote) {
|
|
798
|
-
let backslashes = 0
|
|
799
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
800
|
-
if (backslashes % 2 === 0) quote = null
|
|
801
|
-
}
|
|
802
|
-
continue
|
|
803
|
-
}
|
|
804
|
-
if (c === '"' || c === "'") {
|
|
805
|
-
quote = c
|
|
806
|
-
continue
|
|
807
|
-
}
|
|
808
|
-
if (c === '{' || c === '[') depth++
|
|
809
|
-
else if (c === '}' || c === ']') depth--
|
|
810
|
-
else if (c === ',' && depth === 0) {
|
|
811
|
-
segments.push(inner.slice(start, i))
|
|
812
|
-
start = i + 1
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
segments.push(inner.slice(start))
|
|
816
|
-
return segments
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Unescape a JS string-literal body (the content between the
|
|
821
|
-
* matching opening and closing quotes, not the quotes themselves).
|
|
822
|
-
* Handles the common single-character escapes `\\`, `\'`, `\"`,
|
|
823
|
-
* `\n`, `\r`, `\t`, `\0`, and the backslash-anything fallback that
|
|
824
|
-
* mirrors JS's "unknown escape is the character itself" semantics.
|
|
825
|
-
* Hex / unicode / octal escapes are intentionally out of scope —
|
|
826
|
-
* the spread-bag fixture corpus uses ASCII identifiers and short
|
|
827
|
-
* literal values, so the harness doesn't need a full JS string
|
|
828
|
-
* decoder (#1413 review).
|
|
829
|
-
*/
|
|
830
|
-
function unescapeJsString(s: string): string {
|
|
831
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
832
|
-
switch (c) {
|
|
833
|
-
case 'n': return '\n'
|
|
834
|
-
case 'r': return '\r'
|
|
835
|
-
case 't': return '\t'
|
|
836
|
-
case '0': return '\0'
|
|
837
|
-
// `\\`, `\'`, `\"`, and any other single-character escape
|
|
838
|
-
// collapse to the literal character (matches JS semantics
|
|
839
|
-
// for unrecognised escapes).
|
|
840
|
-
default: return c
|
|
841
|
-
}
|
|
842
|
-
})
|
|
843
|
-
}
|
|
844
686
|
|
|
845
687
|
/**
|
|
846
688
|
* Perl single-quoted string escape: `'` AND `\` need escaping.
|
|
@@ -886,34 +728,3 @@ function toPerlLiteral(value: unknown): string {
|
|
|
886
728
|
return 'undef'
|
|
887
729
|
}
|
|
888
730
|
|
|
889
|
-
/**
|
|
890
|
-
* Convert a JS literal value to a Perl literal.
|
|
891
|
-
* Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default patterns.
|
|
892
|
-
*/
|
|
893
|
-
function jsToPerlValue(jsValue: string): string | null {
|
|
894
|
-
const v = jsValue.trim()
|
|
895
|
-
|
|
896
|
-
// Number
|
|
897
|
-
if (/^-?\d+(\.\d+)?$/.test(v)) return v
|
|
898
|
-
|
|
899
|
-
// String literal
|
|
900
|
-
if (/^['"].*['"]$/.test(v)) return v
|
|
901
|
-
|
|
902
|
-
// Boolean
|
|
903
|
-
if (v === 'true') return '1'
|
|
904
|
-
if (v === 'false') return '0'
|
|
905
|
-
|
|
906
|
-
// Empty array
|
|
907
|
-
if (v === '[]') return '[]'
|
|
908
|
-
|
|
909
|
-
// props.xxx ?? default — extract the default value
|
|
910
|
-
const nullishMatch = v.match(/\?\?\s*(.+)$/)
|
|
911
|
-
if (nullishMatch) {
|
|
912
|
-
return jsToPerlValue(nullishMatch[1])
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// props.xxx (no default) — return undef
|
|
916
|
-
if (v.startsWith('props.')) return 'undef'
|
|
917
|
-
|
|
918
|
-
return null
|
|
919
|
-
}
|
|
@@ -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
|
-
})
|