@barefootjs/xslate 0.31.3 → 0.31.5
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/index.js +12 -41
- package/dist/adapter/xslate-adapter.d.ts +33 -37
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/index.js +12 -41
- package/dist/vite.js +237 -203
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +5 -5
- package/src/__tests__/xslate-adapter.test.ts +58 -20
- package/src/adapter/xslate-adapter.ts +77 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/xslate",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.5",
|
|
4
4
|
"description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"directory": "packages/adapter-xslate"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.31.
|
|
58
|
+
"@barefootjs/shared": "0.31.5"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
75
|
-
"@barefootjs/jsx": "0.31.
|
|
76
|
-
"@barefootjs/vite": "0.31.
|
|
77
|
-
"@barefootjs/client": "0.31.
|
|
75
|
+
"@barefootjs/jsx": "0.31.5",
|
|
76
|
+
"@barefootjs/vite": "0.31.5",
|
|
77
|
+
"@barefootjs/client": "0.31.5",
|
|
78
78
|
"typescript": "^5.0.0",
|
|
79
79
|
"vite": "^6.0.0"
|
|
80
80
|
}
|
|
@@ -458,10 +458,11 @@ export function Parent() {
|
|
|
458
458
|
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
459
459
|
// substitute an outer const's literal value even at an occurrence that is
|
|
460
460
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
461
|
-
// iteration rendered the same hard-coded literal. Guarded
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
461
|
+
// iteration rendered the same hard-coded literal. Guarded via the threaded,
|
|
462
|
+
// position-accurate `BindingScope` (#2482 Stage 2 — previously a coarse
|
|
463
|
+
// whole-component exclusion, same as #2212's, that also suppressed a
|
|
464
|
+
// genuinely non-shadowed occurrence outside the loop; the threaded scope
|
|
465
|
+
// only shadows AT the enclosing loop's own body).
|
|
465
466
|
describe('XslateAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
466
467
|
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
467
468
|
const { template } = compileAndGenerate(`
|
|
@@ -500,11 +501,11 @@ function Widget({ values }: { values: number[] }) {
|
|
|
500
501
|
expect(template).toContain('1 + 5')
|
|
501
502
|
})
|
|
502
503
|
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
test('a const referenced outside the loop whose name is loop-bound elsewhere
|
|
504
|
+
// Position-accurate scope (#2482 Stage 2): a name that is loop-bound
|
|
505
|
+
// elsewhere in the component but NOT at THIS occurrence still inlines
|
|
506
|
+
// here — the coarse whole-component exclusion this used to hit (and
|
|
507
|
+
// accept as a trade-off) is gone; only the loop's own body shadows.
|
|
508
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => {
|
|
508
509
|
const { template } = compileAndGenerate(`
|
|
509
510
|
function Widget({ values }: { values: number[] }) {
|
|
510
511
|
const label: string = 'x'
|
|
@@ -514,7 +515,7 @@ function Widget({ values }: { values: number[] }) {
|
|
|
514
515
|
</div>
|
|
515
516
|
}
|
|
516
517
|
`)
|
|
517
|
-
expect(template).
|
|
518
|
+
expect(template).toContain("1 + 'x'")
|
|
518
519
|
expect(template).toContain('2 + $label')
|
|
519
520
|
})
|
|
520
521
|
})
|
|
@@ -526,9 +527,8 @@ function Widget({ values }: { values: number[] }) {
|
|
|
526
527
|
// substitute the outer const's member value even at an occurrence that is
|
|
527
528
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
528
529
|
// iteration rendered the same hard-coded literal instead of the per-item
|
|
529
|
-
// value. Guarded
|
|
530
|
-
//
|
|
531
|
-
// never inlines, falling back to the bare `$cfg.x` member expression.
|
|
530
|
+
// value. Guarded via the threaded, position-accurate `BindingScope` (#2482
|
|
531
|
+
// Stage 2), same as #2221's fix above.
|
|
532
532
|
describe('XslateAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
533
533
|
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
534
534
|
const { template } = compileAndGenerate(`
|
|
@@ -553,12 +553,12 @@ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
|
553
553
|
expect(template).toContain("<: 'bg-ghost' :>")
|
|
554
554
|
})
|
|
555
555
|
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
// inlines its member
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
test('a record member referenced outside the loop whose object name is loop-bound elsewhere
|
|
556
|
+
// Position-accurate scope (#2482 Stage 2): an object name that is
|
|
557
|
+
// loop-bound elsewhere in the component but NOT at THIS occurrence still
|
|
558
|
+
// inlines its member lookup here — the coarse whole-component exclusion
|
|
559
|
+
// this used to hit (and accept as a trade-off) is gone; only the loop's
|
|
560
|
+
// own body shadows.
|
|
561
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => {
|
|
562
562
|
const { template } = compileAndGenerate(`
|
|
563
563
|
const cfg = { x: 'outer-lit' }
|
|
564
564
|
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
@@ -568,11 +568,49 @@ function Widget({ rows }: { rows: { x: string }[] }) {
|
|
|
568
568
|
</div>
|
|
569
569
|
}
|
|
570
570
|
`)
|
|
571
|
-
expect(template).
|
|
571
|
+
expect(template).toContain("<: 'outer-lit' :>")
|
|
572
572
|
expect(template).toContain('<: $cfg.x :>')
|
|
573
573
|
})
|
|
574
574
|
})
|
|
575
575
|
|
|
576
|
+
// Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate
|
|
577
|
+
// `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set)
|
|
578
|
+
// was only threaded around `renderChildren(loop.children)` in `renderLoop`
|
|
579
|
+
// — narrower than the coarse set's always-on coverage. Two row-context
|
|
580
|
+
// conversions sit OUTSIDE that window: a `.map()` preamble local's own
|
|
581
|
+
// initializer, and the whole-item-conditional `loop-i:` key anchor
|
|
582
|
+
// (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own
|
|
583
|
+
// bindings — a loop param shadowing a same-named outer const must resolve
|
|
584
|
+
// to the row value there too, exactly as it already does inside the loop
|
|
585
|
+
// body proper. `renderLoop` now enters the row scope before converting
|
|
586
|
+
// either and pops it after, closing the gap.
|
|
587
|
+
describe('XslateAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => {
|
|
588
|
+
test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => {
|
|
589
|
+
const { template } = compileAndGenerate(`
|
|
590
|
+
function Widget({ values }: { values: number[] }) {
|
|
591
|
+
const label: string = 'x'
|
|
592
|
+
return <ul>{values.map((label) => (label > 0 ? <li key={label}>{label}</li> : null))}</ul>
|
|
593
|
+
}
|
|
594
|
+
`)
|
|
595
|
+
expect(template).toContain('$bf.comment("loop-i:" ~ $label)')
|
|
596
|
+
expect(template).not.toContain('$bf.comment("loop-i:" ~ \'x\')')
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => {
|
|
600
|
+
const { template } = compileAndGenerate(`
|
|
601
|
+
function Widget({ values }: { values: number[] }) {
|
|
602
|
+
const label: string = 'x'
|
|
603
|
+
return <ul>{values.map((label) => {
|
|
604
|
+
const cls = label
|
|
605
|
+
return <li key={label} class={cls}>{label}</li>
|
|
606
|
+
})}</ul>
|
|
607
|
+
}
|
|
608
|
+
`)
|
|
609
|
+
expect(template).toContain(': my $cls = $label;')
|
|
610
|
+
expect(template).not.toContain(": my $cls = 'x';")
|
|
611
|
+
})
|
|
612
|
+
})
|
|
613
|
+
|
|
576
614
|
describe('XslateAdapter - scriptAssets (Vite late-binding, PR1)', () => {
|
|
577
615
|
const CLIENT_COMPONENT = `
|
|
578
616
|
'use client'
|
|
@@ -78,8 +78,8 @@ import {
|
|
|
78
78
|
dangerousInnerHtmlMetacharViolation,
|
|
79
79
|
dangerousInnerHtmlDiagnostic,
|
|
80
80
|
resolveStaticLoopSource,
|
|
81
|
-
collectLoopBoundNames,
|
|
82
81
|
derivesScopeFromSlot,
|
|
82
|
+
BindingScope,
|
|
83
83
|
} from '@barefootjs/jsx'
|
|
84
84
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
85
85
|
import ts from 'typescript'
|
|
@@ -246,30 +246,24 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
246
246
|
private localConstants: IRMetadata['localConstants'] = []
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
249
|
+
* The one canonical, position-accurate "names bound by an enclosing loop
|
|
250
|
+
* callback" service (#2482 Stage 2) — replaces the two ad-hoc devices
|
|
251
|
+
* this adapter used to carry side by side: a coarse whole-component
|
|
252
|
+
* shadow-name Set (built once at `generate()` entry, used only to guard
|
|
253
|
+
* static-const inlining) and a ref-counted, position-accurate live map
|
|
254
|
+
* (pushed/popped around `renderLoop`'s body, used for the boolean-prop/
|
|
255
|
+
* nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
256
|
+
* local-const fallback (#2489)). Both consulted the "is this name
|
|
257
|
+
* loop-bound" question with DIFFERENT meanings — the coarse/live drift
|
|
258
|
+
* #2482 Stage 2 ends. Every shadow-guard site now reads this ONE
|
|
259
|
+
* threaded, immutable scope: `enterLoopRow`/pop-by-reference around
|
|
260
|
+
* `renderChildren(loop.children)` in `renderLoop`, mirroring the Stage
|
|
261
|
+
* 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
|
|
262
|
+
* structurally satisfies `LoopBindingSource`
|
|
263
|
+
* (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
|
|
264
|
+
* the loop node straight to `enterLoopRow`.
|
|
256
265
|
*/
|
|
257
|
-
private
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Names currently bound by an enclosing loop body — the block-param
|
|
261
|
-
* locals `renderLoop` introduces (item, index, per-binding destructure
|
|
262
|
-
* fields, `.map()` preamble declarations) — ref-counted so nested loops
|
|
263
|
-
* compose. This is the POSITION-ACCURATE twin of the coarse
|
|
264
|
-
* `staticLoopSourceBoundNames` above: that set is a whole-component
|
|
265
|
-
* union used only to guard static-const inlining (safe to over-suppress
|
|
266
|
-
* there — the fallback is just "don't inline"), whereas the boolean-prop
|
|
267
|
-
* and nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
268
|
-
* local-const fallback (#2489) need to know whether a name is loop-bound
|
|
269
|
-
* AT THIS POSITION, because for those the coarse fallback would silently
|
|
270
|
-
* mis-render a genuine occurrence of the same name outside the loop too.
|
|
271
|
-
*/
|
|
272
|
-
private loopBoundNames: Map<string, number> = new Map()
|
|
266
|
+
private scope: BindingScope = BindingScope.EMPTY
|
|
273
267
|
|
|
274
268
|
/**
|
|
275
269
|
* Optional, no-default props that are `undef` when the caller omits them.
|
|
@@ -303,8 +297,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
303
297
|
// Per-compile prop classifications (see `props/prop-classes.ts`).
|
|
304
298
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
305
299
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
306
|
-
this.
|
|
307
|
-
this.loopBoundNames.clear()
|
|
300
|
+
this.scope = BindingScope.EMPTY
|
|
308
301
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
309
302
|
this.stringValueNames = collectStringValueNames(ir)
|
|
310
303
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -808,9 +801,11 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
808
801
|
// bare identifier expression below, same as before #2208 — which
|
|
809
802
|
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
810
803
|
// const reference (a loud, conservative refusal, not a silent wrong
|
|
811
|
-
// value).
|
|
804
|
+
// value). Canonical, position-accurate predicate (#2482 Stage 2) — the
|
|
805
|
+
// enclosing loop scope's own membership, not a coarse whole-component
|
|
806
|
+
// union.
|
|
812
807
|
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
813
|
-
isNameShadowed:
|
|
808
|
+
isNameShadowed: this.scope.asShadowPredicate(),
|
|
814
809
|
})
|
|
815
810
|
const staticArray = staticItems !== null ? staticValueToKolon(staticItems) : null
|
|
816
811
|
|
|
@@ -925,6 +920,34 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
925
920
|
// loop-child naming convention). renderedChildren above was computed with
|
|
926
921
|
// the previous flag; recompute under the loop flag.
|
|
927
922
|
|
|
923
|
+
// This loop's row scope, for the position-accurate shadow guard
|
|
924
|
+
// (#2488/#2489, canonicalized on `BindingScope` in #2482 Stage 2).
|
|
925
|
+
// `IRLoop` already structurally satisfies `LoopBindingSource`
|
|
926
|
+
// (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
|
|
927
|
+
// binds exactly the for-header target(s), each destructure binding,
|
|
928
|
+
// the index (when present), and the `.map()` preamble's declared
|
|
929
|
+
// locals, mirroring what THIS renderLoop's for-header + `indexLocalLines`
|
|
930
|
+
// actually introduce. Restored by reference (immutable — no ref-count
|
|
931
|
+
// bookkeeping) so nested loops compose for free. Entered BEFORE
|
|
932
|
+
// `preambleLines`/`bodyChildren` are converted and popped AFTER, not
|
|
933
|
+
// just around `renderChildren` — Copilot review on #2600 caught that
|
|
934
|
+
// the narrower window silently regressed the module-const shadow guard
|
|
935
|
+
// (`_resolveLiteralConst`/`_resolveStaticRecordLiteral`, now
|
|
936
|
+
// scope-driven instead of the old coarse whole-component set) for two
|
|
937
|
+
// row-context conversions that sit OUTSIDE `renderChildren`: a
|
|
938
|
+
// `.map()` preamble local's own initializer (`d.raw`, below) and the
|
|
939
|
+
// whole-item-conditional `loop-i:` key anchor (`loop.key`, in
|
|
940
|
+
// `bodyChildren` below) — both genuinely evaluate PER ROW and must see
|
|
941
|
+
// the row's own bindings. `loop.filterPredicate` deliberately stays
|
|
942
|
+
// OUTSIDE this window (further down, after the pop): per the Stage 0
|
|
943
|
+
// design a filter/sort callback's own param is a separate `callback`
|
|
944
|
+
// frame, never folded into the `.map()` row — and empirically
|
|
945
|
+
// `XslateFilterEmitter` (the filter predicate's dedicated,
|
|
946
|
+
// self-contained emitter) never consults `this.scope` at all, so its
|
|
947
|
+
// position relative to the pop is inert either way.
|
|
948
|
+
const prevScope = this.scope
|
|
949
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
950
|
+
|
|
928
951
|
// Per-row locals for a `.map()` callback preamble (#2447), in source
|
|
929
952
|
// order so a later initializer sees an earlier local — same as the
|
|
930
953
|
// source block. Phase 1 refuses the loop outright when the preamble
|
|
@@ -933,40 +956,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
933
956
|
const preambleLines = (loop.preamble?.declarations ?? []).map(
|
|
934
957
|
d => `: my $${d.name} = ${this.convertExpressionToKolon(d.raw, d.valueParsed)};`,
|
|
935
958
|
)
|
|
936
|
-
// Names this loop binds in body scope, for the position-accurate
|
|
937
|
-
// `loopBoundNames` guard (#2488) — mirrors the ERB adapter's `loopBound`
|
|
938
|
-
// derivation, adapted to what THIS renderLoop actually binds: the
|
|
939
|
-
// for-header target(s) (`loopVar` / `objectIteration` key+value /
|
|
940
|
-
// `iterationShape === 'keys'`'s `param`), each `indexLocalLines` name
|
|
941
|
-
// (explicit index, or a destructure binding), and the `.map()`
|
|
942
|
-
// preamble's declared locals. Ref-counted so nested loops compose.
|
|
943
|
-
const loopBound: string[] = []
|
|
944
|
-
if (loop.objectIteration === 'entries') {
|
|
945
|
-
// `.kv()` binds the pair var the key/value locals are derived from (#2488).
|
|
946
|
-
loopBound.push('__bf_pair', loop.index ?? param, param)
|
|
947
|
-
} else if (loop.objectIteration === 'keys' || loop.objectIteration === 'values') {
|
|
948
|
-
loopBound.push(param)
|
|
949
|
-
} else if (loop.iterationShape === 'keys') {
|
|
950
|
-
// The header still binds the throwaway loop var; `param` is only the
|
|
951
|
-
// index alias derived from it beneath (#2488).
|
|
952
|
-
loopBound.push('__bf_item', param)
|
|
953
|
-
} else if (supportableDestructure) {
|
|
954
|
-
loopBound.push('__bf_item', ...(loop.paramBindings ?? []).map(b => b.name))
|
|
955
|
-
if (loop.index) loopBound.push(loop.index)
|
|
956
|
-
} else {
|
|
957
|
-
loopBound.push(param)
|
|
958
|
-
if (loop.index) loopBound.push(loop.index)
|
|
959
|
-
}
|
|
960
|
-
for (const d of loop.preamble?.declarations ?? []) loopBound.push(d.name)
|
|
961
|
-
for (const n of loopBound) {
|
|
962
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
963
|
-
}
|
|
964
959
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
965
|
-
for (const n of loopBound) {
|
|
966
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
967
|
-
if (c <= 0) this.loopBoundNames.delete(n)
|
|
968
|
-
else this.loopBoundNames.set(n, c)
|
|
969
|
-
}
|
|
970
960
|
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
971
961
|
this.inLoop = prevInLoop
|
|
972
962
|
void renderedChildren
|
|
@@ -974,11 +964,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
974
964
|
// Whole-item conditional: prepend an always-present `<!--bf-loop-i:KEY-->`
|
|
975
965
|
// anchor before each item's (possibly empty) conditional content so the
|
|
976
966
|
// client's `mapArrayAnchored` can hydrate every SSR-rendered item by its
|
|
977
|
-
// anchor.
|
|
967
|
+
// anchor. Still under the row scope (see above) — `loop.key` is a
|
|
968
|
+
// per-row expression.
|
|
978
969
|
const bodyChildren =
|
|
979
970
|
loop.bodyIsItemConditional && loop.key
|
|
980
971
|
? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>\n${childrenUnderLoop}`
|
|
981
972
|
: childrenUnderLoop
|
|
973
|
+
this.scope = prevScope
|
|
982
974
|
|
|
983
975
|
const lines: string[] = []
|
|
984
976
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
@@ -1441,13 +1433,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1441
1433
|
// function-scope (`!isModule`) consts whose value is NOT itself a bare
|
|
1442
1434
|
// identifier (loop guard) are considered.
|
|
1443
1435
|
//
|
|
1444
|
-
// `
|
|
1445
|
-
// param can shadow this outer const's name (`.map((attrs) => <p
|
|
1436
|
+
// `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
|
|
1437
|
+
// own param can shadow this outer const's name (`.map((attrs) => <p
|
|
1446
1438
|
// {...attrs} />)`) — without the guard this forwarded the OUTER
|
|
1447
1439
|
// const's value at every iteration instead of the per-item value.
|
|
1448
|
-
// Must be the
|
|
1449
|
-
//
|
|
1450
|
-
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.
|
|
1440
|
+
// Must be the threaded, position-accurate scope — the same name
|
|
1441
|
+
// spread at ROOT (no enclosing loop) must still resolve the const.
|
|
1442
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
|
|
1451
1443
|
const localConst = (this.localConstants ?? []).find(
|
|
1452
1444
|
c => c.name === trimmed && !c.isModule,
|
|
1453
1445
|
)
|
|
@@ -1845,9 +1837,9 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1845
1837
|
return this.booleanTypedProps.has(bare)
|
|
1846
1838
|
}
|
|
1847
1839
|
|
|
1848
|
-
/** Position-accurate loop-bound-name check — see `
|
|
1840
|
+
/** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */
|
|
1849
1841
|
private isLoopBoundName(name: string): boolean {
|
|
1850
|
-
return this.
|
|
1842
|
+
return this.scope.isBound(name)
|
|
1851
1843
|
}
|
|
1852
1844
|
|
|
1853
1845
|
/**
|
|
@@ -1857,17 +1849,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1857
1849
|
* stash, so a bare `$totalPages` renders empty.
|
|
1858
1850
|
*
|
|
1859
1851
|
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1860
|
-
* name
|
|
1861
|
-
* inlines (#2221) — the occurrence may be the
|
|
1862
|
-
* binding, and substituting the outer const's
|
|
1863
|
-
* iteration with the same hard-coded literal.
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
* `collectStringValueNames`.
|
|
1852
|
+
* name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/
|
|
1853
|
+
* preamble param never inlines (#2221) — the occurrence may be the
|
|
1854
|
+
* loop's own (shadowing) binding, and substituting the outer const's
|
|
1855
|
+
* value there renders every iteration with the same hard-coded literal.
|
|
1856
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a
|
|
1857
|
+
* same-named const elsewhere in the component, outside any loop that
|
|
1858
|
+
* shadows it, still inlines.
|
|
1868
1859
|
*/
|
|
1869
1860
|
private _resolveLiteralConst(name: string): string | null {
|
|
1870
|
-
if (this.
|
|
1861
|
+
if (this.scope.isBound(name)) return null
|
|
1871
1862
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1872
1863
|
if (c?.value === undefined) return null
|
|
1873
1864
|
const v = c.value.trim()
|
|
@@ -1885,15 +1876,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1885
1876
|
* scope, so an enclosing loop callback's own param of the same name
|
|
1886
1877
|
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
1887
1878
|
* still resolved to the OUTER const's member value at every iteration
|
|
1888
|
-
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
* correctly at the
|
|
1879
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
1880
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed
|
|
1881
|
+
* as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY
|
|
1882
|
+
* ENCLOSING loop binds never inlines, falling back to the bare `$cfg.x`
|
|
1883
|
+
* member expression (which an Xslate `: for` loop binds correctly at the
|
|
1884
|
+
* shadowed occurrence); a same-named const elsewhere, outside any loop
|
|
1885
|
+
* that shadows it, still inlines.
|
|
1893
1886
|
*/
|
|
1894
1887
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1895
|
-
|
|
1896
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1888
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
|
|
1897
1889
|
if (!hit) return null
|
|
1898
1890
|
return hit.kind === 'number'
|
|
1899
1891
|
? hit.text
|