@barefootjs/erb 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/erb-adapter.d.ts +7 -0
- package/dist/adapter/erb-adapter.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +2 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +76 -11
- package/dist/adapter/lib/static-value.d.ts +17 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +5 -4
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +76 -11
- package/dist/index.js +77 -17
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/erb-adapter.test.ts +138 -0
- package/src/adapter/erb-adapter.ts +94 -12
- package/src/adapter/expr/emitters.ts +14 -1
- package/src/adapter/lib/static-value.ts +43 -0
- package/src/adapter/props/prop-classes.ts +6 -5
- package/src/conformance-pins.ts +23 -23
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +13 -136
package/src/conformance-pins.ts
CHANGED
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
13
13
|
|
|
14
14
|
export const conformancePins: ConformancePins = {
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
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 Ruby array/hash
|
|
24
|
+
// literal in the loop-bound expression, the same way a module-scope
|
|
25
|
+
// const's value is already seeded.
|
|
25
26
|
// `static-array-from-props` / `static-array-from-props-with-component`:
|
|
26
27
|
// the `.map(([emoji, users]) => …)` / `.map(([id, t]) => …)` callback is
|
|
27
28
|
// a plain array-index destructure (the `.filter(...)` runs on a
|
|
@@ -45,8 +46,9 @@ export const conformancePins: ConformancePins = {
|
|
|
45
46
|
'static-array-from-props': [
|
|
46
47
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
|
|
47
48
|
],
|
|
49
|
+
// BF103 (imported child in the loop body) no longer fires now that the
|
|
50
|
+
// conformance harness passes `siblingTemplatesRegistered: true` (#2205).
|
|
48
51
|
'static-array-from-props-with-component': [
|
|
49
|
-
{ code: 'BF103', severity: 'error' },
|
|
50
52
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
|
|
51
53
|
],
|
|
52
54
|
// #2087 Phase B: `isLowerableLoopDestructure` now admits every fixed-
|
|
@@ -91,16 +93,14 @@ export const conformancePins: ConformancePins = {
|
|
|
91
93
|
// `/* @client */` keyed-map slot-id elision contract only (same as
|
|
92
94
|
// `todo-app`), not a render or BF101 gap.
|
|
93
95
|
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
// silently renders tags as text.
|
|
105
|
-
'dangerous-inner-html': [{ 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' }],
|
|
106
106
|
}
|
|
@@ -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
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* regression test.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
|
|
39
|
+
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
|
|
40
40
|
import type { ComponentIR, SsrDefault } from '@barefootjs/jsx'
|
|
41
41
|
import { mkdir, rm } from 'node:fs/promises'
|
|
42
42
|
import { resolve } from 'node:path'
|
|
@@ -151,8 +151,15 @@ export async function renderErbComponent(options: RenderOptions): Promise<string
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Compile parent source.
|
|
155
|
-
|
|
154
|
+
// Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
|
|
155
|
+
// matches this harness's real behavior — every sibling child template is registered
|
|
156
|
+
// alongside the parent before rendering, so a loop-body cross-template
|
|
157
|
+
// call resolves at render time (#2205).
|
|
158
|
+
const result = compileJSX(source, 'component.tsx', {
|
|
159
|
+
adapter,
|
|
160
|
+
outputIR: true,
|
|
161
|
+
siblingTemplatesRegistered: Boolean(components),
|
|
162
|
+
})
|
|
156
163
|
|
|
157
164
|
const errors = result.errors.filter(e => e.severity === 'error')
|
|
158
165
|
if (errors.length > 0) {
|
|
@@ -495,9 +502,9 @@ function buildRubyProps(
|
|
|
495
502
|
for (const param of ir.metadata.propsParams) {
|
|
496
503
|
if (props && param.name in props) continue
|
|
497
504
|
if (param.defaultValue) {
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
obj[param.name] = value
|
|
505
|
+
const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
|
|
506
|
+
if (result.ok) {
|
|
507
|
+
obj[param.name] = result.value
|
|
501
508
|
continue
|
|
502
509
|
}
|
|
503
510
|
}
|
|
@@ -556,133 +563,3 @@ function buildRubyProps(
|
|
|
556
563
|
return { obj, needsSearchParams }
|
|
557
564
|
}
|
|
558
565
|
|
|
559
|
-
/**
|
|
560
|
-
* Best-effort literal evaluation of a prop-destructure default's source
|
|
561
|
-
* text (`{ size = 'md' }` → `'md'`), including a `props.x ?? default`
|
|
562
|
-
* nullish fallback (handled generically, though destructure defaults
|
|
563
|
-
* rarely reference `props`) and delegating to `parseLiteral` for
|
|
564
|
-
* everything else. Returns `undefined` for a non-literal (computed)
|
|
565
|
-
* default, matching the Perl harnesses' "fall through to undef" behaviour.
|
|
566
|
-
*/
|
|
567
|
-
function jsDefaultLiteral(expr: string): unknown {
|
|
568
|
-
const t = expr.trim()
|
|
569
|
-
const nullishMatch = t.match(/\?\?\s*(.+)$/)
|
|
570
|
-
if (nullishMatch) return jsDefaultLiteral(nullishMatch[1])
|
|
571
|
-
if (t.startsWith('props.')) return undefined
|
|
572
|
-
const parsed = parseLiteral(t)
|
|
573
|
-
return parsed === null && t !== 'null' ? undefined : parsed
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Evaluate a signal initializer expression using provided props.
|
|
578
|
-
* Handles: props.initial ?? 0, props.value, literal values.
|
|
579
|
-
*/
|
|
580
|
-
export function evaluateSignalInit(
|
|
581
|
-
expr: string,
|
|
582
|
-
props?: Record<string, unknown>,
|
|
583
|
-
): unknown {
|
|
584
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
585
|
-
if (nullishMatch) {
|
|
586
|
-
const propName = nullishMatch[1]
|
|
587
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
588
|
-
if (props && propName in props) return props[propName]
|
|
589
|
-
return parseLiteral(defaultExpr)
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
593
|
-
if (propsMatch) {
|
|
594
|
-
if (props && propsMatch[1] in props) return props[propsMatch[1]]
|
|
595
|
-
return null
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return parseLiteral(expr)
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function parseLiteral(expr: string): unknown {
|
|
602
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
603
|
-
if (expr === 'true') return true
|
|
604
|
-
if (expr === 'false') return false
|
|
605
|
-
if (expr === '[]') return []
|
|
606
|
-
|
|
607
|
-
{
|
|
608
|
-
const t = expr.trim()
|
|
609
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
610
|
-
const inner = t.slice(1, -1).trim()
|
|
611
|
-
if (!inner) return []
|
|
612
|
-
const out: unknown[] = []
|
|
613
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
614
|
-
if (!seg.trim()) continue
|
|
615
|
-
const parsed = parseLiteral(seg.trim())
|
|
616
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
617
|
-
out.push(parsed)
|
|
618
|
-
}
|
|
619
|
-
return out
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
624
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
625
|
-
|
|
626
|
-
const trimmed = expr.trim()
|
|
627
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
628
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
629
|
-
if (!inner) return {}
|
|
630
|
-
const obj: Record<string, unknown> = {}
|
|
631
|
-
for (const pair of splitTopLevelCommas(inner)) {
|
|
632
|
-
if (!pair.trim()) continue
|
|
633
|
-
const colonIdx = pair.indexOf(':')
|
|
634
|
-
if (colonIdx < 0) return null
|
|
635
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
636
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
637
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
638
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
639
|
-
const parsedVal = parseLiteral(val)
|
|
640
|
-
if (parsedVal === null && val !== 'null') return null
|
|
641
|
-
obj[key] = parsedVal
|
|
642
|
-
}
|
|
643
|
-
return obj
|
|
644
|
-
}
|
|
645
|
-
return null
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
649
|
-
const segments: string[] = []
|
|
650
|
-
let depth = 0
|
|
651
|
-
let start = 0
|
|
652
|
-
let quote: string | null = null
|
|
653
|
-
for (let i = 0; i < inner.length; i++) {
|
|
654
|
-
const c = inner[i]
|
|
655
|
-
if (quote) {
|
|
656
|
-
if (c === quote) {
|
|
657
|
-
let backslashes = 0
|
|
658
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
659
|
-
if (backslashes % 2 === 0) quote = null
|
|
660
|
-
}
|
|
661
|
-
continue
|
|
662
|
-
}
|
|
663
|
-
if (c === '"' || c === "'") {
|
|
664
|
-
quote = c
|
|
665
|
-
continue
|
|
666
|
-
}
|
|
667
|
-
if (c === '{' || c === '[') depth++
|
|
668
|
-
else if (c === '}' || c === ']') depth--
|
|
669
|
-
else if (c === ',' && depth === 0) {
|
|
670
|
-
segments.push(inner.slice(start, i))
|
|
671
|
-
start = i + 1
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
segments.push(inner.slice(start))
|
|
675
|
-
return segments
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function unescapeJsString(s: string): string {
|
|
679
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
680
|
-
switch (c) {
|
|
681
|
-
case 'n': return '\n'
|
|
682
|
-
case 'r': return '\r'
|
|
683
|
-
case 't': return '\t'
|
|
684
|
-
case '0': return '\0'
|
|
685
|
-
default: return c
|
|
686
|
-
}
|
|
687
|
-
})
|
|
688
|
-
}
|