@barefootjs/xslate 0.17.1 → 0.18.1
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 +3 -1
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +7 -3
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +90 -30
- package/dist/adapter/xslate-adapter.d.ts +30 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +90 -30
- 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 +109 -30
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +13 -103
- package/src/adapter/expr/array-method.ts +22 -1
- package/src/adapter/expr/emitters.ts +23 -11
- package/src/adapter/xslate-adapter.ts +232 -48
- 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'
|
|
@@ -578,33 +621,79 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
578
621
|
return `<: $bf.comment("loop:${loop.markerId}") | mark_raw :><: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`
|
|
579
622
|
}
|
|
580
623
|
|
|
581
|
-
//
|
|
582
|
-
// `({
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
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).
|
|
590
644
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0)
|
|
591
|
-
const supportableDestructure = destructure &&
|
|
645
|
+
const supportableDestructure = destructure && isLowerableLoopDestructure(loop)
|
|
592
646
|
if (destructure && !supportableDestructure) {
|
|
593
647
|
this.errors.push({
|
|
594
648
|
code: 'BF104',
|
|
595
649
|
severity: 'error',
|
|
596
|
-
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.`,
|
|
597
651
|
loc: loop.loc ?? { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
598
652
|
suggestion: {
|
|
599
653
|
message:
|
|
600
654
|
`Options:\n` +
|
|
601
|
-
` 1.
|
|
602
|
-
` 2.
|
|
603
|
-
` 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.`,
|
|
604
659
|
},
|
|
605
660
|
})
|
|
606
661
|
}
|
|
607
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
|
+
|
|
608
697
|
const rawArray = this.convertExpressionToKolon(loop.array)
|
|
609
698
|
// Apply sort if present: wrap the loop array in the shared `$bf.sort`
|
|
610
699
|
// helper, binding the sorted result to a per-iteration local so the
|
|
@@ -642,8 +731,20 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
642
731
|
// Index alias: when an explicit `index` param is present (`.map((x, i) =>
|
|
643
732
|
// ...)`) or the iteration is `keys`-shaped, expose it via a `: my` Kolon
|
|
644
733
|
// local bound to the loop variable's `.index` accessor. A supported
|
|
645
|
-
// destructure param adds one `: my` local per binding
|
|
646
|
-
//
|
|
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.
|
|
647
748
|
const indexLocalLines: string[] = []
|
|
648
749
|
if (loop.iterationShape === 'keys') {
|
|
649
750
|
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`)
|
|
@@ -652,11 +753,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
652
753
|
}
|
|
653
754
|
if (supportableDestructure) {
|
|
654
755
|
for (const b of loop.paramBindings ?? []) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
)
|
|
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
|
+
}
|
|
660
767
|
}
|
|
661
768
|
}
|
|
662
769
|
|
|
@@ -750,11 +857,11 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
750
857
|
},
|
|
751
858
|
emitSpread: (value) => {
|
|
752
859
|
// Kolon hashrefs can't be splatted into the entry list the way Perl
|
|
753
|
-
// `%{...}` flattens into a list.
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
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.
|
|
758
865
|
return this.convertExpressionToKolon(value.expr)
|
|
759
866
|
},
|
|
760
867
|
emitTemplate: (value, name) =>
|
|
@@ -766,41 +873,106 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
766
873
|
emitJsxChildren: () => '',
|
|
767
874
|
}
|
|
768
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
|
+
|
|
769
931
|
renderComponent(comp: IRComponent): string {
|
|
770
|
-
|
|
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
|
+
|
|
771
936
|
for (const p of comp.props) {
|
|
772
937
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
773
938
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
774
939
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
775
|
-
// Spread props: enumerate the analyzer's props params into hashref
|
|
776
|
-
// entries (the propsObject case) — Kolon can't flatten a hashref into
|
|
777
|
-
// the entry list. Other spread shapes are refused with BF101.
|
|
778
940
|
if (p.value.kind === 'spread') {
|
|
779
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.
|
|
780
947
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
781
948
|
for (const pp of this.propsParams) {
|
|
782
|
-
|
|
949
|
+
currentEntries().push(`${pp.name} => $${pp.name}`)
|
|
783
950
|
}
|
|
784
951
|
continue
|
|
785
952
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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) })
|
|
795
964
|
continue
|
|
796
965
|
}
|
|
797
966
|
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name)
|
|
798
|
-
if (lowered)
|
|
967
|
+
if (lowered) currentEntries().push(lowered)
|
|
799
968
|
}
|
|
800
969
|
// Pass slot ID so the child renderer can set correct scope ID for
|
|
801
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.
|
|
802
974
|
if (comp.slotId && !this.inLoop) {
|
|
803
|
-
|
|
975
|
+
currentEntries().push(`_bf_slot => '${comp.slotId}'`)
|
|
804
976
|
}
|
|
805
977
|
const tplName = this.toTemplateName(comp.name)
|
|
806
978
|
|
|
@@ -822,12 +994,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
822
994
|
const childrenBody = this.renderChildren(effectiveChildren)
|
|
823
995
|
this.inLoop = prevInLoop
|
|
824
996
|
const macroName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
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 :>`
|
|
828
1000
|
}
|
|
829
1001
|
|
|
830
|
-
const
|
|
1002
|
+
const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
|
|
1003
|
+
const hashEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
|
|
831
1004
|
return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`
|
|
832
1005
|
}
|
|
833
1006
|
|
|
@@ -1327,6 +1500,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1327
1500
|
const qArgs = queryHrefArgs(node, n => this.renderParsedExprToKolon(n))
|
|
1328
1501
|
return `$bf.query(${qArgs.join(', ')})`
|
|
1329
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
|
+
}
|
|
1330
1514
|
}
|
|
1331
1515
|
}
|
|
1332
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