@barefootjs/blade 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/blade-adapter.d.ts +48 -0
- package/dist/adapter/blade-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +83 -6
- package/dist/adapter/lib/static-value.d.ts +13 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +29 -11
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +83 -6
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +85 -15
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/blade-adapter-unit.test.ts +120 -0
- package/src/__tests__/blade-adapter.test.ts +13 -0
- package/src/adapter/blade-adapter.ts +110 -5
- package/src/adapter/lib/static-value.ts +39 -0
- package/src/adapter/props/prop-classes.ts +34 -12
- package/src/conformance-pins.ts +25 -28
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +10 -119
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sets the adapter consults during lowering. No adapter instance state.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type
|
|
9
|
+
import { collectLoopBoundNames, type ComponentIR } from '@barefootjs/jsx'
|
|
10
10
|
import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -24,8 +24,9 @@ export function collectBooleanTypedProps(ir: ComponentIR): Set<string> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Bare references to
|
|
28
|
-
* textarea's `rows`) are
|
|
27
|
+
* Bare references to presence-uncertain no-default props (non-primitive
|
|
28
|
+
* typed OR declared optional, #2259 — e.g. textarea's `rows`) are
|
|
29
|
+
* `null` when omitted → guarded with
|
|
29
30
|
* `is defined and is not null` in `emitExpression`. See the
|
|
30
31
|
* `nullableOptionalProps` field docstring in `blade-adapter.ts`.
|
|
31
32
|
*/
|
|
@@ -36,21 +37,38 @@ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
|
|
|
36
37
|
p =>
|
|
37
38
|
p.defaultValue === undefined &&
|
|
38
39
|
!p.isRest &&
|
|
39
|
-
p.type?.kind !== 'primitive',
|
|
40
|
+
(p.type?.kind !== 'primitive' || p.optional),
|
|
40
41
|
)
|
|
41
42
|
.map(p => p.name),
|
|
42
43
|
)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* String-typed signals
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
47
|
+
* String-typed signals, props, and same-file local consts (#2212). A
|
|
48
|
+
* signal is string-typed when its inferred type is `string` (or,
|
|
49
|
+
* defensively, when its initial value is a bare string literal); a prop
|
|
50
|
+
* when its annotated type is `string`; a local const the same way. Consumed
|
|
51
|
+
* by `isStringConcatBinary`/`isStringTypedOperand` (`@barefootjs/jsx`) to
|
|
52
|
+
* pick `.` over JS `+`'s numeric fallback (#2163, #2212) — including now
|
|
53
|
+
* for a bare identifier operand, not just a prop/getter/literal. In the
|
|
54
|
+
* Mojo adapter this ALSO drives `eq`/`ne` selection for string equality;
|
|
55
|
+
* the Blade emitters don't consume the distinction there — `===`/`!==`
|
|
56
|
+
* ALWAYS route through `bf.eq`/`bf.neq` regardless of operand type (see
|
|
57
|
+
* `expr/emitters.ts`'s file header, divergence 4) — so that half of this
|
|
58
|
+
* set is carried only for parity with the Perl-family adapters.
|
|
59
|
+
*
|
|
60
|
+
* Excludes any name bound as a `.map()`/`.filter()` loop callback's item
|
|
61
|
+
* or index parameter ANYWHERE in the component (Fable review, #2212): the
|
|
62
|
+
* lookup below is a flat, scope-blind `Set<string>` with no notion of a
|
|
63
|
+
* loop param shadowing an outer string-typed binding of the same name
|
|
64
|
+
* (`items.map((name) => 1 + name)` inside a component that also has a
|
|
65
|
+
* string `name` prop) — left unguarded, that shadowed `name` would be
|
|
66
|
+
* misdetected as string-typed and `1 + name` would silently lower to `.`
|
|
67
|
+
* instead of staying numeric `+`. Subtracting loop-bound names is coarse
|
|
68
|
+
* (it also suppresses a genuinely non-shadowed same-named string
|
|
69
|
+
* elsewhere in the component) but safe: the suppressed case just falls
|
|
70
|
+
* back to today's numeric `+` — the same, already-accepted residual as an
|
|
71
|
+
* unresolvable operand — never silently-wrong output.
|
|
54
72
|
*/
|
|
55
73
|
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
56
74
|
const names = new Set<string>()
|
|
@@ -62,5 +80,9 @@ export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
|
62
80
|
for (const p of ir.metadata.propsParams) {
|
|
63
81
|
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
64
82
|
}
|
|
83
|
+
for (const c of ir.metadata.localConstants) {
|
|
84
|
+
if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value)) names.add(c.name)
|
|
85
|
+
}
|
|
86
|
+
for (const bound of collectLoopBoundNames(ir)) names.delete(bound)
|
|
65
87
|
return names
|
|
66
88
|
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
12
12
|
|
|
13
13
|
export const conformancePins: ConformancePins = {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
15
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
16
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
17
|
+
// BF103 loop-body cross-template check no longer fires spuriously. (Both
|
|
18
|
+
// fixtures are still skipped on this adapter via `render-divergences.ts`
|
|
19
|
+
// — #2209 — for an unrelated signal-seeding gap.)
|
|
20
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
21
|
+
// array-literal initializer is now recognized as fully-static
|
|
22
|
+
// (`resolveStaticLoopSource`) and inlined as a native PHP array literal
|
|
23
|
+
// in the `@foreach` header, the same way a module-scope const's value is
|
|
24
|
+
// already seeded.
|
|
23
25
|
// The `([emoji, users]) => …` array-destructure param itself now lowers
|
|
24
26
|
// (#2087 Phase B — see the destructure comment below), but the loop
|
|
25
27
|
// ARRAY is a function-scope computed const (`const entries =
|
|
@@ -28,12 +30,10 @@ export const conformancePins: ConformancePins = {
|
|
|
28
30
|
// check and policy as Jinja / ERB) instead of silently iterating zero
|
|
29
31
|
// times over an unbound name.
|
|
30
32
|
'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ code: 'BF101', severity: 'error' },
|
|
36
|
-
],
|
|
33
|
+
// BF101 (computed local-const loop array, as above) fires; BF103
|
|
34
|
+
// (imported child in the loop body) no longer does now that the
|
|
35
|
+
// conformance harness passes `siblingTemplatesRegistered: true` (#2205).
|
|
36
|
+
'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
|
|
37
37
|
// #2087 Phase B: every `.map()` destructure shape in the shared corpus
|
|
38
38
|
// now lowers on Blade via an `@php(...)` local built from the binding's
|
|
39
39
|
// structured `segments` path (`bladeLoopBindingAccessor` in
|
|
@@ -76,17 +76,14 @@ export const conformancePins: ConformancePins = {
|
|
|
76
76
|
// / etc. via the same evaluator-JSON mechanism as `.filter` / `.every` /
|
|
77
77
|
// `.some`, so they render. Only the NESTED-in-a-predicate form above is
|
|
78
78
|
// refused (#2038).
|
|
79
|
-
//
|
|
80
|
-
// callback
|
|
81
|
-
// (`
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
// the shape loudly instead of emitting entity-escaped markup that
|
|
90
|
-
// silently renders tags as text.
|
|
91
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
79
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
80
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
81
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
82
|
+
// #2090 established for `.sort(fnref)`.
|
|
83
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
84
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
85
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
86
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
87
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
88
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
92
89
|
}
|
|
@@ -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
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* caller's `props` and needs no JSON round-trip.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
|
|
41
|
+
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
|
|
42
42
|
import type { ComponentIR } from '@barefootjs/jsx'
|
|
43
43
|
import { mkdir, rm } from 'node:fs/promises'
|
|
44
44
|
import { resolve } from 'node:path'
|
|
@@ -157,8 +157,15 @@ export async function renderBladeComponent(options: RenderOptions): Promise<stri
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Compile parent source.
|
|
161
|
-
|
|
160
|
+
// Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
|
|
161
|
+
// matches this harness's real behavior — every sibling child template is registered
|
|
162
|
+
// alongside the parent before rendering, so a loop-body cross-template
|
|
163
|
+
// call resolves at render time (#2205).
|
|
164
|
+
const result = compileJSX(source, 'component.tsx', {
|
|
165
|
+
adapter,
|
|
166
|
+
outputIR: true,
|
|
167
|
+
siblingTemplatesRegistered: Boolean(components),
|
|
168
|
+
})
|
|
162
169
|
|
|
163
170
|
const errors = result.errors.filter(e => e.severity === 'error')
|
|
164
171
|
if (errors.length > 0) {
|
|
@@ -585,122 +592,6 @@ function buildBladeProps(
|
|
|
585
592
|
return { data, literalAssignments }
|
|
586
593
|
}
|
|
587
594
|
|
|
588
|
-
/**
|
|
589
|
-
* Evaluate a signal initializer expression using provided props.
|
|
590
|
-
* Handles: props.initial ?? 0, props.value, literal values.
|
|
591
|
-
*
|
|
592
|
-
* Language-agnostic (returns a real JS value, not source text) — shared
|
|
593
|
-
* verbatim with the Jinja harness's helper of the same name.
|
|
594
|
-
*/
|
|
595
|
-
export function evaluateSignalInit(
|
|
596
|
-
expr: string,
|
|
597
|
-
props?: Record<string, unknown>,
|
|
598
|
-
): unknown {
|
|
599
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
600
|
-
if (nullishMatch) {
|
|
601
|
-
const propName = nullishMatch[1]
|
|
602
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
603
|
-
if (props && propName in props) return props[propName]
|
|
604
|
-
return parseLiteral(defaultExpr)
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
608
|
-
if (propsMatch) {
|
|
609
|
-
if (props && propsMatch[1] in props) return props[propsMatch[1]]
|
|
610
|
-
return null
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return parseLiteral(expr)
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function parseLiteral(expr: string): unknown {
|
|
617
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
618
|
-
if (expr === 'true') return true
|
|
619
|
-
if (expr === 'false') return false
|
|
620
|
-
if (expr === '[]') return []
|
|
621
|
-
|
|
622
|
-
{
|
|
623
|
-
const t = expr.trim()
|
|
624
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
625
|
-
const inner = t.slice(1, -1).trim()
|
|
626
|
-
if (!inner) return []
|
|
627
|
-
const out: unknown[] = []
|
|
628
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
629
|
-
if (!seg.trim()) continue
|
|
630
|
-
const parsed = parseLiteral(seg.trim())
|
|
631
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
632
|
-
out.push(parsed)
|
|
633
|
-
}
|
|
634
|
-
return out
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
639
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
640
|
-
|
|
641
|
-
const trimmed = expr.trim()
|
|
642
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
643
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
644
|
-
if (!inner) return {}
|
|
645
|
-
const obj: Record<string, unknown> = {}
|
|
646
|
-
for (const pair of splitTopLevelCommas(inner)) {
|
|
647
|
-
if (!pair.trim()) continue
|
|
648
|
-
const colonIdx = pair.indexOf(':')
|
|
649
|
-
if (colonIdx < 0) return null
|
|
650
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
651
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
652
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
653
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
654
|
-
const parsedVal = parseLiteral(val)
|
|
655
|
-
if (parsedVal === null && val !== 'null') return null
|
|
656
|
-
obj[key] = parsedVal
|
|
657
|
-
}
|
|
658
|
-
return obj
|
|
659
|
-
}
|
|
660
|
-
return null
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
664
|
-
const segments: string[] = []
|
|
665
|
-
let depth = 0
|
|
666
|
-
let start = 0
|
|
667
|
-
let quote: string | null = null
|
|
668
|
-
for (let i = 0; i < inner.length; i++) {
|
|
669
|
-
const c = inner[i]
|
|
670
|
-
if (quote) {
|
|
671
|
-
if (c === quote) {
|
|
672
|
-
let backslashes = 0
|
|
673
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
674
|
-
if (backslashes % 2 === 0) quote = null
|
|
675
|
-
}
|
|
676
|
-
continue
|
|
677
|
-
}
|
|
678
|
-
if (c === '"' || c === "'") {
|
|
679
|
-
quote = c
|
|
680
|
-
continue
|
|
681
|
-
}
|
|
682
|
-
if (c === '{' || c === '[') depth++
|
|
683
|
-
else if (c === '}' || c === ']') depth--
|
|
684
|
-
else if (c === ',' && depth === 0) {
|
|
685
|
-
segments.push(inner.slice(start, i))
|
|
686
|
-
start = i + 1
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
segments.push(inner.slice(start))
|
|
690
|
-
return segments
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function unescapeJsString(s: string): string {
|
|
694
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
695
|
-
switch (c) {
|
|
696
|
-
case 'n': return '\n'
|
|
697
|
-
case 'r': return '\r'
|
|
698
|
-
case 't': return '\t'
|
|
699
|
-
case '0': return '\0'
|
|
700
|
-
default: return c
|
|
701
|
-
}
|
|
702
|
-
})
|
|
703
|
-
}
|
|
704
595
|
|
|
705
596
|
/**
|
|
706
597
|
* Convert PascalCase to snake_case for template naming (matches the
|