@barefootjs/xslate 0.17.0 → 0.18.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/expr/array-method.d.ts +13 -1
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +13 -10
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +132 -85
- package/dist/adapter/lib/ir-scope.d.ts +0 -5
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -1
- package/dist/adapter/memo/seed.d.ts +14 -14
- package/dist/adapter/memo/seed.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts +30 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +132 -85
- package/dist/conformance-pins.d.ts +10 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -85
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +138 -91
- package/src/adapter/expr/array-method.ts +43 -1
- package/src/adapter/expr/emitters.ts +53 -23
- package/src/adapter/lib/ir-scope.ts +0 -11
- package/src/adapter/memo/seed.ts +25 -72
- package/src/adapter/xslate-adapter.ts +254 -52
- package/src/conformance-pins.ts +112 -0
- package/src/index.ts +1 -0
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
TypeInfo,
|
|
41
41
|
TemplatePrimitiveRegistry,
|
|
42
42
|
IRMetadata,
|
|
43
|
+
LoopBindingPathSegment,
|
|
43
44
|
} from '@barefootjs/jsx'
|
|
44
45
|
import {
|
|
45
46
|
BaseAdapter,
|
|
@@ -64,13 +65,14 @@ import {
|
|
|
64
65
|
collectModuleStringConsts,
|
|
65
66
|
extractArrowBodyExpression,
|
|
66
67
|
collectContextConsumers,
|
|
67
|
-
isLowerableObjectRestDestructure,
|
|
68
68
|
type ContextConsumer,
|
|
69
69
|
lookupStaticRecordLiteral,
|
|
70
70
|
searchParamsLocalNames,
|
|
71
71
|
prepareLoweringMatchers,
|
|
72
72
|
queryHrefArgs,
|
|
73
|
+
isValidHelperId,
|
|
73
74
|
sortComparatorFromArrow,
|
|
75
|
+
isLowerableLoopDestructure,
|
|
74
76
|
} from '@barefootjs/jsx'
|
|
75
77
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
76
78
|
import ts from 'typescript'
|
|
@@ -79,7 +81,7 @@ import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
|
79
81
|
|
|
80
82
|
import type { XslateRenderCtx } from './lib/types.ts'
|
|
81
83
|
import { XSLATE_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
82
|
-
import { kolonHashKey } from './lib/kolon-naming.ts'
|
|
84
|
+
import { kolonHashKey, escapeKolonSingleQuoted } from './lib/kolon-naming.ts'
|
|
83
85
|
import {
|
|
84
86
|
resolveJsxChildrenProp,
|
|
85
87
|
collectRootScopeNodes,
|
|
@@ -108,6 +110,47 @@ import {
|
|
|
108
110
|
export type { XslateAdapterOptions } from './lib/types.ts'
|
|
109
111
|
import type { XslateAdapterOptions } from './lib/types.ts'
|
|
110
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Build a Kolon accessor expression for a `.map()` destructure binding's
|
|
115
|
+
* structured `segments` path (#2087 Phase B), walking `field` (`.key` for an
|
|
116
|
+
* identifier-safe name, `["key"]` — quoted with `kolonHashKey`'s convention —
|
|
117
|
+
* otherwise, since Kolon parses `$x.data-priority` as a subtraction) /
|
|
118
|
+
* `index` (`[N]`) steps onto `base`. Verified against real Text::Xslate that
|
|
119
|
+
* dot- and bracket-access chain freely (`$x.cells[0]`, `$x["cells"][0]`).
|
|
120
|
+
* Never string-parses `LoopParamBinding.path` — see the repo-wide rule
|
|
121
|
+
* against regex-parsing JS/TS-derived syntax.
|
|
122
|
+
*
|
|
123
|
+
* Used both for a fixed binding's FULL accessor (`base` = `$__bf_item`,
|
|
124
|
+
* `segments` = the whole path) and a rest binding's PARENT-prefix accessor
|
|
125
|
+
* (`segments` may be empty, at the loop root, in which case this returns
|
|
126
|
+
* `base` unchanged) — see `LoopParamBinding.segments` jsdoc for which case
|
|
127
|
+
* a binding is in.
|
|
128
|
+
*/
|
|
129
|
+
function kolonSegmentAccessor(base: string, segments: readonly LoopBindingPathSegment[]): string {
|
|
130
|
+
let expr = base
|
|
131
|
+
for (const seg of segments) {
|
|
132
|
+
expr +=
|
|
133
|
+
seg.kind === 'field'
|
|
134
|
+
? seg.isIdent
|
|
135
|
+
? `.${seg.key}`
|
|
136
|
+
: `[${kolonHashKey(seg.key)}]`
|
|
137
|
+
: `[${seg.index}]`
|
|
138
|
+
}
|
|
139
|
+
return expr
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Quote a string as a Kolon single-quoted literal, unconditionally — unlike
|
|
144
|
+
* `kolonHashKey` (which leaves an identifier-safe name unquoted for `key =>
|
|
145
|
+
* val` hashref-literal position), a plain array-literal element
|
|
146
|
+
* (`$bf.omit($x, [id, title])`) is NOT hash-key position: Kolon has no
|
|
147
|
+
* bareword-as-string convention there, so every object-rest exclude key
|
|
148
|
+
* needs an explicit string literal.
|
|
149
|
+
*/
|
|
150
|
+
function kolonStringLiteral(s: string): string {
|
|
151
|
+
return `'${escapeKolonSingleQuoted(s)}'`
|
|
152
|
+
}
|
|
153
|
+
|
|
111
154
|
export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRenderCtx> {
|
|
112
155
|
name = 'xslate'
|
|
113
156
|
extension = '.tx'
|
|
@@ -568,36 +611,89 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
568
611
|
// ===========================================================================
|
|
569
612
|
|
|
570
613
|
renderLoop(loop: IRLoop): string {
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
//
|
|
582
|
-
// rest
|
|
614
|
+
// clientOnly loops must not render items at SSR time, but must still emit
|
|
615
|
+
// the `loop:`/`/loop:` boundary marker pair (Hono and Go parity) so the
|
|
616
|
+
// client runtime's mapArray() can locate the insertion anchor when
|
|
617
|
+
// hydrating the array. Without the markers, mapArray() resolves
|
|
618
|
+
// anchor = null and appends after sibling markers (#872). The marker id
|
|
619
|
+
// disambiguates sibling `.map()` calls under the same parent (#1087).
|
|
620
|
+
if (loop.clientOnly) {
|
|
621
|
+
return `<: $bf.comment("loop:${loop.markerId}") | mark_raw :><: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// A `.map()` destructure loop param (`([k, v]) => ...` / `({ id, user: {
|
|
625
|
+
// name } }) => ...` / `({ id, ...rest }) => ...`) lowers to a Kolon `: my`
|
|
626
|
+
// local per binding, walking each binding's structured `segments` path
|
|
627
|
+
// (#2087 Phase B) into a native `.key` / `["key"]` / `[N]` accessor off
|
|
628
|
+
// the per-item var — so the body's `$id` / `$name` / `$rest.flag` /
|
|
629
|
+
// `{...rest}` all resolve natively, at any nesting depth.
|
|
630
|
+
//
|
|
631
|
+
// Check the IR's structured `paramBindings` field rather than
|
|
632
|
+
// string-matching `loop.param`: Phase 1 populates `paramBindings`
|
|
633
|
+
// iff the param is a destructure pattern (array or object); a
|
|
634
|
+
// simple identifier leaves it `undefined`. The structured check is
|
|
635
|
+
// robust to whitespace / formatting variants in the source.
|
|
636
|
+
//
|
|
637
|
+
// `isLowerableLoopDestructure` (#2087) still refuses: an object-rest
|
|
638
|
+
// binding used any way other than member access (`rest.flag`) or a
|
|
639
|
+
// `{...rest}` spread onto an intrinsic element (that needs the actual
|
|
640
|
+
// residual *object*, which isn't always safe to materialize — see the
|
|
641
|
+
// gate's own jsdoc for the full list); a `.filter().map(destructure)`
|
|
642
|
+
// chain (the filter-param rewrite is out of scope here); and a
|
|
643
|
+
// computed property key (`{ [k]: v }`, refused earlier as BF025).
|
|
583
644
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0)
|
|
584
|
-
const supportableDestructure = destructure &&
|
|
645
|
+
const supportableDestructure = destructure && isLowerableLoopDestructure(loop)
|
|
585
646
|
if (destructure && !supportableDestructure) {
|
|
586
647
|
this.errors.push({
|
|
587
648
|
code: 'BF104',
|
|
588
649
|
severity: 'error',
|
|
589
|
-
message: `Loop callback uses
|
|
650
|
+
message: `Loop callback uses a destructure pattern (\`${loop.param}\`) that the Xslate adapter cannot lower — see the diagnostic detail for the specific shape.`,
|
|
590
651
|
loc: loop.loc ?? { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
591
652
|
suggestion: {
|
|
592
653
|
message:
|
|
593
654
|
`Options:\n` +
|
|
594
|
-
` 1.
|
|
595
|
-
` 2.
|
|
596
|
-
` 3.
|
|
655
|
+
` 1. If this is an object-rest binding (\`{ ...rest }\`), only reading \`rest.field\` or spreading \`{...rest}\` onto an intrinsic element lowers — other uses (passing \`rest\` to a function, rendering it as text) need the client runtime.\n` +
|
|
656
|
+
` 2. If this is chained \`.filter().map(({ ... }) => ...)\`, hoist the destructure into a variable inside the callback body instead.\n` +
|
|
657
|
+
` 3. Mark the loop position as @client-only so the destructure runs in JS on the client.\n` +
|
|
658
|
+
` 4. Move the loop into a primitive that the adapter registers explicitly.`,
|
|
597
659
|
},
|
|
598
660
|
})
|
|
599
661
|
}
|
|
600
662
|
|
|
663
|
+
// A `.map()` loop whose array is a bare identifier bound to a
|
|
664
|
+
// FUNCTION-scope local const with a non-statically-evaluable initializer
|
|
665
|
+
// that reads props/signals (e.g. `const entries =
|
|
666
|
+
// Object.entries(props.x ?? {}).filter(...)`) can't render correctly.
|
|
667
|
+
// Module-scope consts (`isModule`, e.g. `const payments = [...]` at the
|
|
668
|
+
// top of the file) are a DIFFERENT, already-working case handled
|
|
669
|
+
// elsewhere. Function-scope locals get no per-render stash slot — this
|
|
670
|
+
// adapter's only "elsewhere" for a local const is inlining its value at
|
|
671
|
+
// the use site (`_resolveLiteralConst`'s numeric/single-quoted-string
|
|
672
|
+
// fast path, or a static-record-literal lookup), never binding one as a
|
|
673
|
+
// `: my` template local. Left unchecked, `: for $entries -> $__bf_item {`
|
|
674
|
+
// over an undeclared `$entries` faults at request time instead of
|
|
675
|
+
// failing loudly at build time. Pre-existing, general limitation,
|
|
676
|
+
// orthogonal to #2087's destructure-binding work — newly reachable in
|
|
677
|
+
// this adapter's test corpus only because the widened destructure gate
|
|
678
|
+
// (#2087 Phase A/B) no longer refuses this fixture's `([emoji, users])
|
|
679
|
+
// => ...` param first.
|
|
680
|
+
const arrayName = loop.array.trim()
|
|
681
|
+
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
682
|
+
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
683
|
+
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
684
|
+
this.errors.push({
|
|
685
|
+
code: 'BF101',
|
|
686
|
+
severity: 'error',
|
|
687
|
+
message: `Loop array \`${arrayName}\` is a local computed value (\`${arrayConst.value}\`) that the Xslate adapter cannot bind as a template variable — only numeric/string-literal locals inline at their use site.`,
|
|
688
|
+
loc: loop.loc ?? { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
689
|
+
suggestion: {
|
|
690
|
+
message:
|
|
691
|
+
'Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client.',
|
|
692
|
+
},
|
|
693
|
+
})
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
601
697
|
const rawArray = this.convertExpressionToKolon(loop.array)
|
|
602
698
|
// Apply sort if present: wrap the loop array in the shared `$bf.sort`
|
|
603
699
|
// helper, binding the sorted result to a per-iteration local so the
|
|
@@ -635,8 +731,20 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
635
731
|
// Index alias: when an explicit `index` param is present (`.map((x, i) =>
|
|
636
732
|
// ...)`) or the iteration is `keys`-shaped, expose it via a `: my` Kolon
|
|
637
733
|
// local bound to the loop variable's `.index` accessor. A supported
|
|
638
|
-
// destructure param adds one `: my` local per binding
|
|
639
|
-
//
|
|
734
|
+
// destructure param adds one `: my` local per binding, walking each
|
|
735
|
+
// binding's `segments` path (#2087 Phase B):
|
|
736
|
+
// - fixed (`b.rest` unset): the FULL accessor from `$__bf_item`.
|
|
737
|
+
// - array-rest: `$bf.slice(parent, from, nil)` — the same runtime
|
|
738
|
+
// helper `.slice()` JS-method calls lower to (see `array-method.ts`),
|
|
739
|
+
// so the "no end → to length" arithmetic stays in one place. Kolon's
|
|
740
|
+
// undefined literal is `nil`, not Perl's `undef`.
|
|
741
|
+
// - object-rest: `$bf.omit(parent, [...excluded keys...])` — a TRUE
|
|
742
|
+
// residual hashref (not the whole item aliased), so both
|
|
743
|
+
// `$rest.flag` (member-access use) and `$bf.spread_attrs($rest)`
|
|
744
|
+
// (spread-onto-element use) see only the non-destructured keys.
|
|
745
|
+
// `parent` is `$__bf_item` walked through the binding's PARENT-prefix
|
|
746
|
+
// `segments` (empty at the loop root, per the `LoopParamBinding` jsdoc) —
|
|
747
|
+
// NOT the same as a fixed binding's full-accessor segments.
|
|
640
748
|
const indexLocalLines: string[] = []
|
|
641
749
|
if (loop.iterationShape === 'keys') {
|
|
642
750
|
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`)
|
|
@@ -645,11 +753,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
645
753
|
}
|
|
646
754
|
if (supportableDestructure) {
|
|
647
755
|
for (const b of loop.paramBindings ?? []) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
)
|
|
756
|
+
const parent = kolonSegmentAccessor(`$${loopVar}`, b.segments ?? [])
|
|
757
|
+
if (b.rest?.kind === 'object') {
|
|
758
|
+
const exclude = b.rest.exclude.map(k => kolonStringLiteral(k.key)).join(', ')
|
|
759
|
+
indexLocalLines.push(`: my $${b.name} = $bf.omit(${parent}, [${exclude}]);`)
|
|
760
|
+
} else if (b.rest?.kind === 'array') {
|
|
761
|
+
indexLocalLines.push(`: my $${b.name} = $bf.slice(${parent}, ${b.rest.from}, nil);`)
|
|
762
|
+
} else {
|
|
763
|
+
indexLocalLines.push(
|
|
764
|
+
`: my $${b.name} = ${kolonSegmentAccessor(`$${loopVar}`, b.segments ?? [])};`,
|
|
765
|
+
)
|
|
766
|
+
}
|
|
653
767
|
}
|
|
654
768
|
}
|
|
655
769
|
|
|
@@ -743,11 +857,11 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
743
857
|
},
|
|
744
858
|
emitSpread: (value) => {
|
|
745
859
|
// Kolon hashrefs can't be splatted into the entry list the way Perl
|
|
746
|
-
// `%{...}` flattens into a list.
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
860
|
+
// `%{...}` flattens into a list. `renderComponent` handles EVERY
|
|
861
|
+
// spread shape itself (both the enumerated propsObject case and the
|
|
862
|
+
// general chained `.merge(...)` fold — see its own docstring), so
|
|
863
|
+
// this callback is never reached for `kind: 'spread'` props; it
|
|
864
|
+
// only exists to satisfy the `AttrValueEmitter` interface.
|
|
751
865
|
return this.convertExpressionToKolon(value.expr)
|
|
752
866
|
},
|
|
753
867
|
emitTemplate: (value, name) =>
|
|
@@ -759,41 +873,106 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
759
873
|
emitJsxChildren: () => '',
|
|
760
874
|
}
|
|
761
875
|
|
|
876
|
+
/**
|
|
877
|
+
* A `renderComponent` props hashref, built as an ORDERED sequence of
|
|
878
|
+
* segments so `{...before, ...spread, after: 1}` JSX spread semantics
|
|
879
|
+
* (later entries win) survive the trip through Kolon, which has no
|
|
880
|
+
* hash-splat syntax (no `%$h`-into-hashref-literal form — verified: parse
|
|
881
|
+
* error). Each `'entries'` segment is a literal Kolon hashref
|
|
882
|
+
* `{ k => v, ... }`; each `'spread'` segment is an arbitrary expression
|
|
883
|
+
* lowered from a `{...expr}` prop. `combineComponentPropSegments` folds
|
|
884
|
+
* the sequence into ONE expression via chained `.merge(...)` calls
|
|
885
|
+
* (Kolon's builtin hash method, later argument wins on key conflict,
|
|
886
|
+
* matching `Object.assign`/JSX order).
|
|
887
|
+
*/
|
|
888
|
+
private componentPropSegmentEntries(
|
|
889
|
+
segments: Array<{ kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }>,
|
|
890
|
+
): string[] {
|
|
891
|
+
const last = segments[segments.length - 1]
|
|
892
|
+
if (last && last.kind === 'entries') return last.parts
|
|
893
|
+
const seg = { kind: 'entries' as const, parts: [] as string[] }
|
|
894
|
+
segments.push(seg)
|
|
895
|
+
return seg.parts
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Fold ordered prop segments into a single Kolon expression via chained
|
|
900
|
+
* `.merge(...)` calls — Kolon's builtin hash method, later argument wins
|
|
901
|
+
* on key conflict, exactly like `{...a, ...b}`. A spread segment's
|
|
902
|
+
* expression is wrapped `(EXPR // {})` before merging: `.merge(undef)`
|
|
903
|
+
* warns "Merging value is not a HASH reference" (verified against real
|
|
904
|
+
* Text::Xslate 3.5.9), so the defined-or guard normalises a missing bag
|
|
905
|
+
* (e.g. `$children.props` when `children` was never passed — Kolon
|
|
906
|
+
* tolerates the chained dot-access on an undefined value itself,
|
|
907
|
+
* verified empirically) to an empty hashref first. If the fold starts
|
|
908
|
+
* with a spread segment, the base is just `($SPREAD // {})`; a
|
|
909
|
+
* following segment chains `.merge({...})` onto it. Empty `'entries'`
|
|
910
|
+
* segments are dropped so a leading/trailing spread doesn't drag in a
|
|
911
|
+
* needless `{}.merge(...)`. Returns `'{}'` when every segment is empty
|
|
912
|
+
* (no props at all).
|
|
913
|
+
*/
|
|
914
|
+
private combineComponentPropSegments(
|
|
915
|
+
segments: ReadonlyArray<{ kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }>,
|
|
916
|
+
): string {
|
|
917
|
+
let acc: string | null = null
|
|
918
|
+
for (const seg of segments) {
|
|
919
|
+
if (seg.kind === 'entries') {
|
|
920
|
+
if (seg.parts.length === 0) continue
|
|
921
|
+
const text = `{ ${seg.parts.join(', ')} }`
|
|
922
|
+
acc = acc === null ? text : `${acc}.merge(${text})`
|
|
923
|
+
} else {
|
|
924
|
+
const text = `(${seg.expr} // {})`
|
|
925
|
+
acc = acc === null ? text : `${acc}.merge(${text})`
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return acc ?? '{}'
|
|
929
|
+
}
|
|
930
|
+
|
|
762
931
|
renderComponent(comp: IRComponent): string {
|
|
763
|
-
|
|
932
|
+
type Segment = { kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }
|
|
933
|
+
const segments: Segment[] = [{ kind: 'entries', parts: [] }]
|
|
934
|
+
const currentEntries = () => this.componentPropSegmentEntries(segments)
|
|
935
|
+
|
|
764
936
|
for (const p of comp.props) {
|
|
765
937
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
766
938
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
767
939
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
768
|
-
// Spread props: enumerate the analyzer's props params into hashref
|
|
769
|
-
// entries (the propsObject case) — Kolon can't flatten a hashref into
|
|
770
|
-
// the entry list. Other spread shapes are refused with BF101.
|
|
771
940
|
if (p.value.kind === 'spread') {
|
|
772
941
|
const trimmed = p.value.expr.trim()
|
|
942
|
+
// SolidJS-style props identifier (`function(props: P)`) has no
|
|
943
|
+
// matching runtime hash in Kolon scope — props arrive as a flat
|
|
944
|
+
// set of top-level template vars, so enumerate the
|
|
945
|
+
// analyzer-extracted props params into hashref entries instead of
|
|
946
|
+
// treating it as a runtime spread expression.
|
|
773
947
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
774
948
|
for (const pp of this.propsParams) {
|
|
775
|
-
|
|
949
|
+
currentEntries().push(`${pp.name} => $${pp.name}`)
|
|
776
950
|
}
|
|
777
951
|
continue
|
|
778
952
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
953
|
+
// Every other spread shape (a destructure rest-bag `props`, a
|
|
954
|
+
// member-access bag like `children.props`, an intrinsic-element
|
|
955
|
+
// spread helper's own operand, …) — Kolon hashref literals can't
|
|
956
|
+
// splat a runtime hash into named entries at a call site, but the
|
|
957
|
+
// builtin `.merge` method can fold it into the accumulated
|
|
958
|
+
// hashref at the right ordinal position, mirroring Twig's
|
|
959
|
+
// `|merge` / Jinja's `dict(base, **top)`: no compile-time
|
|
960
|
+
// filtering of onXxx/ref keys out of the runtime bag (the render
|
|
961
|
+
// contract tolerates them, same as the other spread-lowering
|
|
962
|
+
// adapters).
|
|
963
|
+
segments.push({ kind: 'spread', expr: this.convertExpressionToKolon(p.value.expr) })
|
|
788
964
|
continue
|
|
789
965
|
}
|
|
790
966
|
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name)
|
|
791
|
-
if (lowered)
|
|
967
|
+
if (lowered) currentEntries().push(lowered)
|
|
792
968
|
}
|
|
793
969
|
// Pass slot ID so the child renderer can set correct scope ID for
|
|
794
970
|
// hydration. Skip for loop children — they use ComponentName_random.
|
|
971
|
+
// Appended to whatever the trailing entries segment is so a spread's
|
|
972
|
+
// own `_bf_slot`/`children` keys (if any) never win over these
|
|
973
|
+
// compiler-controlled entries.
|
|
795
974
|
if (comp.slotId && !this.inLoop) {
|
|
796
|
-
|
|
975
|
+
currentEntries().push(`_bf_slot => '${comp.slotId}'`)
|
|
797
976
|
}
|
|
798
977
|
const tplName = this.toTemplateName(comp.name)
|
|
799
978
|
|
|
@@ -815,12 +994,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
815
994
|
const childrenBody = this.renderChildren(effectiveChildren)
|
|
816
995
|
this.inLoop = prevInLoop
|
|
817
996
|
const macroName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}',
|
|
997
|
+
currentEntries().push(`children => ${macroName}()`)
|
|
998
|
+
const dict = this.combineComponentPropSegments(segments)
|
|
999
|
+
return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`
|
|
821
1000
|
}
|
|
822
1001
|
|
|
823
|
-
const
|
|
1002
|
+
const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
|
|
1003
|
+
const hashEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
|
|
824
1004
|
return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`
|
|
825
1005
|
}
|
|
826
1006
|
|
|
@@ -1147,7 +1327,18 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1147
1327
|
param: string,
|
|
1148
1328
|
localVarMap: Map<string, string> = new Map(),
|
|
1149
1329
|
): string {
|
|
1150
|
-
return emitParsedExpr(
|
|
1330
|
+
return emitParsedExpr(
|
|
1331
|
+
expr,
|
|
1332
|
+
new XslateFilterEmitter(
|
|
1333
|
+
param,
|
|
1334
|
+
localVarMap,
|
|
1335
|
+
n => this._isStringValueName(n),
|
|
1336
|
+
// A nested callback method inside the predicate has no Kolon scalar
|
|
1337
|
+
// form — surface BF101 (#2038) instead of silently degrading it to
|
|
1338
|
+
// its receiver.
|
|
1339
|
+
(message, reason) => this._recordExprBF101(message, reason),
|
|
1340
|
+
),
|
|
1341
|
+
)
|
|
1151
1342
|
}
|
|
1152
1343
|
|
|
1153
1344
|
// ===========================================================================
|
|
@@ -1309,6 +1500,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1309
1500
|
const qArgs = queryHrefArgs(node, n => this.renderParsedExprToKolon(n))
|
|
1310
1501
|
return `$bf.query(${qArgs.join(', ')})`
|
|
1311
1502
|
}
|
|
1503
|
+
// Generic `helper-call` (#2069) — the neutral vocabulary's escape
|
|
1504
|
+
// hatch for a userland `LoweringPlugin` that lowers to a single
|
|
1505
|
+
// runtime-helper invocation. `$bf.<helper>(args…)` mirrors the
|
|
1506
|
+
// `query` helper's own naming convention exactly: the framework
|
|
1507
|
+
// renders the call, the plugin author registers `<helper>` as a
|
|
1508
|
+
// Kolon-callable method on the `$bf` vars entry in their own
|
|
1509
|
+
// runtime — same contract as `$bf.query` itself, just not built in.
|
|
1510
|
+
if (node?.kind === 'helper-call' && isValidHelperId(node.helper)) {
|
|
1511
|
+
const argsX = node.args.map(a => this.renderParsedExprToKolon(a))
|
|
1512
|
+
return `$bf.${node.helper}(${argsX.join(', ')})`
|
|
1513
|
+
}
|
|
1312
1514
|
}
|
|
1313
1515
|
}
|
|
1314
1516
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-fixture build-time contracts for shapes the Xslate adapter
|
|
3
|
+
* intentionally refuses to lower. Mirrors mojo's set — the lowering
|
|
4
|
+
* gates are shared code paths in the ported adapter. Consumed by this
|
|
5
|
+
* package's own conformance test (as `expectedDiagnostics`) and by
|
|
6
|
+
* `bf compat` (issue-URL attribution).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ConformancePins } from '@barefootjs/jsx'
|
|
10
|
+
|
|
11
|
+
export const conformancePins: ConformancePins = {
|
|
12
|
+
// Sibling-imported child component in a loop body: emits a
|
|
13
|
+
// cross-template call needing separate registration. BF103 makes
|
|
14
|
+
// the requirement loud (same as mojo).
|
|
15
|
+
'static-array-children': [{ code: 'BF103', severity: 'error' }],
|
|
16
|
+
// TodoApp / TodoAppSSR import `TodoItem` from a sibling file and
|
|
17
|
+
// call it inside a keyed `.map`. With the standalone-filter fix in
|
|
18
|
+
// place these reach the SAME BF103 (imported child in `.map`) as
|
|
19
|
+
// mojo — NOT BF101 — confirming the `.filter(...)` chain itself now
|
|
20
|
+
// lowers and the only remaining gate is the imported-child one.
|
|
21
|
+
'todo-app': [{ code: 'BF103', severity: 'error' }],
|
|
22
|
+
'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
|
|
23
|
+
// `([emoji, users]) => ...` / `([id, t]) => ...` are plain array-index
|
|
24
|
+
// (tuple) destructures, no rest — #2087 Phase B's `segments`-walking
|
|
25
|
+
// accessor lowers both to `$__bf_item[0]` / `$__bf_item[1]` `: my` locals
|
|
26
|
+
// like any other fixed binding, so BF104 no longer fires for either
|
|
27
|
+
// fixture. Each now hits a DIFFERENT, pre-existing, orthogonal gap
|
|
28
|
+
// instead: the loop array (`entries`) is a function-scope local const
|
|
29
|
+
// with a computed initializer (`Object.entries(props.x ?? {}).filter(...)`)
|
|
30
|
+
// that the adapter can't bind as a template variable — see the dedicated
|
|
31
|
+
// `arrayConst` BF101 check in `renderLoop`. This was always true; it was
|
|
32
|
+
// simply unreachable before because BF104 refused the destructure shape
|
|
33
|
+
// first.
|
|
34
|
+
'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
|
|
35
|
+
// Both BF103 (sibling-imported `<Tag>` child component) and the BF101
|
|
36
|
+
// above fire; BF104 no longer does (see above).
|
|
37
|
+
'static-array-from-props-with-component': [
|
|
38
|
+
{ code: 'BF103', severity: 'error' },
|
|
39
|
+
{ code: 'BF101', severity: 'error' },
|
|
40
|
+
],
|
|
41
|
+
// #1310 / #2087: rest destructure in .map() callback. All four shapes now
|
|
42
|
+
// lower via #2087 Phase B's `segments`-walking accessor:
|
|
43
|
+
// - object-rest read via member access (`rest-destructure-object-in-map`):
|
|
44
|
+
// `$bf.omit($__bf_item, [...exclude keys...])`, `$rest.flag` reads the
|
|
45
|
+
// residual hashref.
|
|
46
|
+
// - object-rest spread onto the root element
|
|
47
|
+
// (`rest-destructure-object-spread-in-map`): same `$bf.omit(...)`
|
|
48
|
+
// residual, forwarded via the existing `$bf.spread_attrs($rest)` path.
|
|
49
|
+
// - array-rest (`rest-destructure-array-in-map`): `$bf.slice($__bf_item,
|
|
50
|
+
// N, nil)` — the same runtime helper `.slice()` JS-method calls use.
|
|
51
|
+
// - nested rest inside an object pattern (`rest-destructure-nested-in-map`):
|
|
52
|
+
// the parent-prefix accessor (`$__bf_item.cells`) feeds the same
|
|
53
|
+
// `$bf.slice(...)` call.
|
|
54
|
+
// None of these are pinned here anymore.
|
|
55
|
+
// (button/kbd graduated: the site/ui Button/Kbd `<Slot>` `{...props}` /
|
|
56
|
+
// `{...children.props}` component-spread now lowers via Kolon's builtin
|
|
57
|
+
// `.merge(...)` method chain — see `xslate-adapter.ts`'s
|
|
58
|
+
// `renderComponent` — instead of refusing with BF101, so these two no
|
|
59
|
+
// longer need a pin here.)
|
|
60
|
+
// #1467 demo-corpus context providers (`radio-group`, `select`,
|
|
61
|
+
// `dropdown-menu`, `combobox`, `command`) are no longer pinned — an
|
|
62
|
+
// object-literal provider value (`{ value: currentValue,
|
|
63
|
+
// onValueChange: (v) => {…} }`) lowers to a Kolon hashref via
|
|
64
|
+
// `parseProviderObjectLiteral` (#1897): getter members snapshot
|
|
65
|
+
// their body's SSR value, handler / function-shaped members lower
|
|
66
|
+
// to `nil`. The command demo's `ref={(el) => {…}}` function prop on
|
|
67
|
+
// an imported component is skipped at SSR like `on*` handlers.
|
|
68
|
+
//
|
|
69
|
+
// #1467 Phase 2e: `data-table` is no longer pinned here — it
|
|
70
|
+
// compiles clean now (`selected()[index]` → `index-access`,
|
|
71
|
+
// `.toFixed(2)` → `$bf.to_fixed`, `/* @client */` memo SSR-folded)
|
|
72
|
+
// and renders to Hono parity on real Text::Xslate. The keyed-loop
|
|
73
|
+
// scope-ID divergence (#1896) was fixed by the body-children
|
|
74
|
+
// `inLoop` reset (loop-item children get `_bf_slot`); data-table is
|
|
75
|
+
// off `skipJsx` entirely and only kept in `skipMarkerConformance`
|
|
76
|
+
// below for the shared `/* @client */` keyed-map slot-id elision
|
|
77
|
+
// contract (same as `todo-app`), not a render or BF101 gap.
|
|
78
|
+
// `style-3-signals` / `style-object-dynamic` no longer pinned — a
|
|
79
|
+
// `style={{ … }}` object literal now lowers to a CSS string with dynamic
|
|
80
|
+
// values interpolated (`background-color:<: $color :>;padding:8px`) via
|
|
81
|
+
// `tryLowerStyleObject` (#1322).
|
|
82
|
+
// (`tagged-template-classname` graduated by #2092 — the tag resolves
|
|
83
|
+
// through the interleave-tag catalogue and desugars to an untagged
|
|
84
|
+
// template literal, so it lowers like any other className template.)
|
|
85
|
+
// #2038: a filter predicate whose body contains a NESTED callback call
|
|
86
|
+
// (`t => !picked().some(p => …)` / `t => picked().find(p => …)`). Kolon
|
|
87
|
+
// has no inline `grep` form, so `XslateFilterEmitter.callbackMethod` used
|
|
88
|
+
// to degrade the inner call to its receiver, silently changing predicate
|
|
89
|
+
// semantics — the compiler is loud instead of lossy. (Mojo is pinned only
|
|
90
|
+
// for the `.find` variant: it lowers a nested `.some` to a real inline
|
|
91
|
+
// Perl `grep`.) The `/* @client */` twin
|
|
92
|
+
// (`filter-nested-callback-predicate-client`) has no pin here: it must
|
|
93
|
+
// render clean on every adapter, which asserts the suppression contract.
|
|
94
|
+
// https://github.com/piconic-ai/barefootjs/issues/2038
|
|
95
|
+
'filter-nested-callback-predicate': [
|
|
96
|
+
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2038' },
|
|
97
|
+
],
|
|
98
|
+
'filter-nested-find-predicate': [
|
|
99
|
+
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2038' },
|
|
100
|
+
],
|
|
101
|
+
// NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
|
|
102
|
+
// (text position) are NOT pinned here — unlike mojo (which refuses them),
|
|
103
|
+
// Xslate lowers them to `$bf.find` / `find_index` / `find_last` /
|
|
104
|
+
// `find_last_index` via the same Kolon-lambda mechanism as `.filter` /
|
|
105
|
+
// `.every` / `.some`, so they render. Only the NESTED-in-a-predicate form
|
|
106
|
+
// above is refused (#2038).
|
|
107
|
+
// #2073 follow-up: a function-reference `.map(format)` callback has no
|
|
108
|
+
// arrow body to serialize — not a CALLBACK_METHODS shape — so the
|
|
109
|
+
// UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
|
|
110
|
+
// a broken template.
|
|
111
|
+
'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
|
|
112
|
+
}
|
package/src/index.ts
CHANGED