@barefootjs/jinja 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/jinja-adapter.d.ts +32 -36
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/index.js +12 -41
- package/dist/vite.js +237 -269
- package/package.json +5 -5
- package/src/__tests__/jinja-adapter-unit.test.ts +59 -21
- package/src/adapter/jinja-adapter.ts +76 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jinja",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.5",
|
|
4
4
|
"description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"directory": "packages/adapter-jinja"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.31.
|
|
56
|
+
"@barefootjs/shared": "0.31.5"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
73
|
-
"@barefootjs/jsx": "0.31.
|
|
74
|
-
"@barefootjs/vite": "0.31.
|
|
75
|
-
"@barefootjs/client": "0.31.
|
|
73
|
+
"@barefootjs/jsx": "0.31.5",
|
|
74
|
+
"@barefootjs/vite": "0.31.5",
|
|
75
|
+
"@barefootjs/client": "0.31.5",
|
|
76
76
|
"typescript": "^5.0.0",
|
|
77
77
|
"vite": "^6.0.0"
|
|
78
78
|
}
|
|
@@ -414,11 +414,12 @@ export function Parent() {
|
|
|
414
414
|
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
415
415
|
// substitute an outer const's literal value even at an occurrence that is
|
|
416
416
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
417
|
-
// iteration rendered the same hard-coded literal. Guarded
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
417
|
+
// iteration rendered the same hard-coded literal. Guarded via the threaded,
|
|
418
|
+
// position-accurate `BindingScope` (#2482 Stage 2 — previously a coarse
|
|
419
|
+
// whole-component exclusion, same as #2212's, that also suppressed a
|
|
420
|
+
// genuinely non-shadowed occurrence outside the loop; the threaded scope
|
|
421
|
+
// only shadows AT the enclosing loop's own body). SSR-only tests for the
|
|
422
|
+
// same #2222 reason as the #2212 describe below.
|
|
422
423
|
describe('JinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
423
424
|
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
424
425
|
const { template } = compileAndGenerate(`
|
|
@@ -457,11 +458,11 @@ function Widget({ values }: { values: number[] }) {
|
|
|
457
458
|
expect(template).toContain('1 + 5')
|
|
458
459
|
})
|
|
459
460
|
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
test('a const referenced outside the loop whose name is loop-bound elsewhere
|
|
461
|
+
// Position-accurate scope (#2482 Stage 2): a name that is loop-bound
|
|
462
|
+
// elsewhere in the component but NOT at THIS occurrence still inlines
|
|
463
|
+
// here — the coarse whole-component exclusion this used to hit (and
|
|
464
|
+
// accept as a trade-off) is gone; only the loop's own body shadows.
|
|
465
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => {
|
|
465
466
|
const { template } = compileAndGenerate(`
|
|
466
467
|
function Widget({ values }: { values: number[] }) {
|
|
467
468
|
const label: string = 'x'
|
|
@@ -471,7 +472,7 @@ function Widget({ values }: { values: number[] }) {
|
|
|
471
472
|
</div>
|
|
472
473
|
}
|
|
473
474
|
`)
|
|
474
|
-
expect(template).
|
|
475
|
+
expect(template).toContain("1 + 'x'")
|
|
475
476
|
expect(template).toContain('2 + label')
|
|
476
477
|
})
|
|
477
478
|
})
|
|
@@ -483,9 +484,8 @@ function Widget({ values }: { values: number[] }) {
|
|
|
483
484
|
// substitute the outer const's member value even at an occurrence that is
|
|
484
485
|
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
485
486
|
// iteration rendered the same hard-coded literal instead of the per-item
|
|
486
|
-
// value. Guarded
|
|
487
|
-
//
|
|
488
|
-
// never inlines, falling back to the bare `cfg['x']` member expression.
|
|
487
|
+
// value. Guarded via the threaded, position-accurate `BindingScope` (#2482
|
|
488
|
+
// Stage 2), same as #2221's fix above.
|
|
489
489
|
describe('JinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
490
490
|
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
491
491
|
const { template } = compileAndGenerate(`
|
|
@@ -510,12 +510,12 @@ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
|
510
510
|
expect(template).toContain("bf.string('bg-ghost')")
|
|
511
511
|
})
|
|
512
512
|
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
// inlines its member
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
test('a record member referenced outside the loop whose object name is loop-bound elsewhere
|
|
513
|
+
// Position-accurate scope (#2482 Stage 2): an object name that is
|
|
514
|
+
// loop-bound elsewhere in the component but NOT at THIS occurrence still
|
|
515
|
+
// inlines its member lookup here — the coarse whole-component exclusion
|
|
516
|
+
// this used to hit (and accept as a trade-off) is gone; only the loop's
|
|
517
|
+
// own body shadows.
|
|
518
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => {
|
|
519
519
|
const { template } = compileAndGenerate(`
|
|
520
520
|
const cfg = { x: 'outer-lit' }
|
|
521
521
|
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
@@ -525,11 +525,49 @@ function Widget({ rows }: { rows: { x: string }[] }) {
|
|
|
525
525
|
</div>
|
|
526
526
|
}
|
|
527
527
|
`)
|
|
528
|
-
expect(template).
|
|
528
|
+
expect(template).toContain("bf.string('outer-lit')")
|
|
529
529
|
expect(template).toContain("bf.string(cfg['x'])")
|
|
530
530
|
})
|
|
531
531
|
})
|
|
532
532
|
|
|
533
|
+
// Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate
|
|
534
|
+
// `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set)
|
|
535
|
+
// was only threaded around `renderChildren(loop.children)` in `renderLoop`
|
|
536
|
+
// — narrower than the coarse set's always-on coverage. Two row-context
|
|
537
|
+
// conversions sit OUTSIDE that window: a `.map()` preamble local's own
|
|
538
|
+
// initializer, and the whole-item-conditional `loop-i:` key anchor
|
|
539
|
+
// (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own
|
|
540
|
+
// bindings — a loop param shadowing a same-named outer const must resolve
|
|
541
|
+
// to the row value there too, exactly as it already does inside the loop
|
|
542
|
+
// body proper. `renderLoop` now enters the row scope before converting
|
|
543
|
+
// either and pops it after, closing the gap.
|
|
544
|
+
describe('JinjaAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => {
|
|
545
|
+
test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => {
|
|
546
|
+
const { template } = compileAndGenerate(`
|
|
547
|
+
function Widget({ values }: { values: number[] }) {
|
|
548
|
+
const label: string = 'x'
|
|
549
|
+
return <ul>{values.map((label) => (label > 0 ? <li key={label}>{label}</li> : null))}</ul>
|
|
550
|
+
}
|
|
551
|
+
`)
|
|
552
|
+
expect(template).toContain('bf.comment("loop-i:" ~ bf.string(label))')
|
|
553
|
+
expect(template).not.toContain("bf.comment(\"loop-i:\" ~ bf.string('x'))")
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => {
|
|
557
|
+
const { template } = compileAndGenerate(`
|
|
558
|
+
function Widget({ values }: { values: number[] }) {
|
|
559
|
+
const label: string = 'x'
|
|
560
|
+
return <ul>{values.map((label) => {
|
|
561
|
+
const cls = label
|
|
562
|
+
return <li key={label} class={cls}>{label}</li>
|
|
563
|
+
})}</ul>
|
|
564
|
+
}
|
|
565
|
+
`)
|
|
566
|
+
expect(template).toContain('{% set cls = label %}')
|
|
567
|
+
expect(template).not.toContain("{% set cls = 'x' %}")
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
533
571
|
// A fragment-rooted component (top-level `<>...</>` return) has no single
|
|
534
572
|
// wrapping element to bound the client's scope query, so `renderFragment`
|
|
535
573
|
// brackets its children with a comment-based scope marker pair instead of
|
|
@@ -145,8 +145,8 @@ import {
|
|
|
145
145
|
dangerousInnerHtmlMetacharViolation,
|
|
146
146
|
dangerousInnerHtmlDiagnostic,
|
|
147
147
|
resolveStaticLoopSource,
|
|
148
|
-
collectLoopBoundNames,
|
|
149
148
|
derivesScopeFromSlot,
|
|
149
|
+
BindingScope,
|
|
150
150
|
} from '@barefootjs/jsx'
|
|
151
151
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
152
152
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
@@ -272,30 +272,24 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
272
272
|
private localConstants: IRMetadata['localConstants'] = []
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
275
|
+
* The one canonical, position-accurate "names bound by an enclosing loop
|
|
276
|
+
* callback" service (#2482 Stage 2) — replaces the two ad-hoc devices
|
|
277
|
+
* this adapter used to carry side by side: a coarse whole-component
|
|
278
|
+
* shadow-name Set (built once at `generate()` entry, used only to guard
|
|
279
|
+
* static-const inlining) and a ref-counted, position-accurate live map
|
|
280
|
+
* (pushed/popped around `renderLoop`'s body, used for the boolean-prop/
|
|
281
|
+
* nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
282
|
+
* local-const fallback (#2489)). Both consulted the "is this name
|
|
283
|
+
* loop-bound" question with DIFFERENT meanings — the coarse/live drift
|
|
284
|
+
* #2482 Stage 2 ends. Every shadow-guard site now reads this ONE
|
|
285
|
+
* threaded, immutable scope: `enterLoopRow`/pop-by-reference around
|
|
286
|
+
* `renderChildren(loop.children)` in `renderLoop`, mirroring the Stage
|
|
287
|
+
* 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
|
|
288
|
+
* structurally satisfies `LoopBindingSource`
|
|
289
|
+
* (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
|
|
290
|
+
* the loop node straight to `enterLoopRow`.
|
|
282
291
|
*/
|
|
283
|
-
private
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Names currently bound by an enclosing loop body — the block-param
|
|
287
|
-
* locals `renderLoop` introduces (item, index, per-binding destructure
|
|
288
|
-
* fields, `.map()` preamble declarations) — ref-counted so nested loops
|
|
289
|
-
* compose. This is the POSITION-ACCURATE twin of the coarse
|
|
290
|
-
* `staticLoopSourceBoundNames` above: that set is a whole-component
|
|
291
|
-
* union used only to guard static-const inlining (safe to over-suppress
|
|
292
|
-
* there — the fallback is just "don't inline"), whereas the boolean-prop
|
|
293
|
-
* and nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
294
|
-
* local-const fallback (#2489) need to know whether a name is loop-bound
|
|
295
|
-
* AT THIS POSITION, because for those the coarse fallback would silently
|
|
296
|
-
* mis-render a genuine occurrence of the same name outside the loop too.
|
|
297
|
-
*/
|
|
298
|
-
private loopBoundNames: Map<string, number> = new Map()
|
|
292
|
+
private scope: BindingScope = BindingScope.EMPTY
|
|
299
293
|
|
|
300
294
|
/**
|
|
301
295
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
@@ -329,8 +323,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
329
323
|
// ("True"/"False") (#1897, pagination's data-active).
|
|
330
324
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
331
325
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
332
|
-
this.
|
|
333
|
-
this.loopBoundNames.clear()
|
|
326
|
+
this.scope = BindingScope.EMPTY
|
|
334
327
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
335
328
|
this.stringValueNames = collectStringValueNames(ir)
|
|
336
329
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -829,9 +822,11 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
829
822
|
// bare identifier expression below, same as before #2208 — which
|
|
830
823
|
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
831
824
|
// const reference (a loud, conservative refusal, not a silent wrong
|
|
832
|
-
// value).
|
|
825
|
+
// value). Canonical, position-accurate predicate (#2482 Stage 2) — the
|
|
826
|
+
// enclosing loop scope's own membership, not a coarse whole-component
|
|
827
|
+
// union.
|
|
833
828
|
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
834
|
-
isNameShadowed:
|
|
829
|
+
isNameShadowed: this.scope.asShadowPredicate(),
|
|
835
830
|
})
|
|
836
831
|
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null
|
|
837
832
|
|
|
@@ -941,6 +936,34 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
941
936
|
// loop-child naming convention). renderedChildren above was computed with
|
|
942
937
|
// the previous flag; recompute under the loop flag.
|
|
943
938
|
|
|
939
|
+
// This loop's row scope, for the position-accurate shadow guard
|
|
940
|
+
// (#2488/#2489, canonicalized on `BindingScope` in #2482 Stage 2).
|
|
941
|
+
// `IRLoop` already structurally satisfies `LoopBindingSource`
|
|
942
|
+
// (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
|
|
943
|
+
// binds exactly the for-header target(s), each destructure binding,
|
|
944
|
+
// the index (when present), and the `.map()` preamble's declared
|
|
945
|
+
// locals, mirroring what THIS renderLoop's for-header + `indexLocalLines`
|
|
946
|
+
// actually introduce. Restored by reference (immutable — no ref-count
|
|
947
|
+
// bookkeeping) so nested loops compose for free. Entered BEFORE
|
|
948
|
+
// `preambleLines`/`bodyChildren` are converted and popped AFTER, not
|
|
949
|
+
// just around `renderChildren` — Copilot review on #2600 caught that
|
|
950
|
+
// the narrower window silently regressed the module-const shadow guard
|
|
951
|
+
// (`_resolveLiteralConst`/`_resolveStaticRecordLiteral`, now
|
|
952
|
+
// scope-driven instead of the old coarse whole-component set) for two
|
|
953
|
+
// row-context conversions that sit OUTSIDE `renderChildren`: a
|
|
954
|
+
// `.map()` preamble local's own initializer (`d.raw`, below) and the
|
|
955
|
+
// whole-item-conditional `loop-i:` key anchor (`loop.key`, in
|
|
956
|
+
// `bodyChildren` below) — both genuinely evaluate PER ROW and must see
|
|
957
|
+
// the row's own bindings. `loop.filterPredicate` deliberately stays
|
|
958
|
+
// OUTSIDE this window (further down, after the pop): per the Stage 0
|
|
959
|
+
// design a filter/sort callback's own param is a separate `callback`
|
|
960
|
+
// frame, never folded into the `.map()` row — and empirically
|
|
961
|
+
// `JinjaFilterEmitter` (the filter predicate's dedicated,
|
|
962
|
+
// self-contained emitter) never consults `this.scope` at all, so its
|
|
963
|
+
// position relative to the pop is inert either way.
|
|
964
|
+
const prevScope = this.scope
|
|
965
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
966
|
+
|
|
944
967
|
// Per-row locals for a `.map()` callback preamble (#2447), in source
|
|
945
968
|
// order so a later initializer sees an earlier local — same as the
|
|
946
969
|
// source block. Phase 1 refuses the loop outright when the preamble
|
|
@@ -949,39 +972,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
949
972
|
const preambleLines = (loop.preamble?.declarations ?? []).map(
|
|
950
973
|
d => `{% set ${jinjaIdent(d.name)} = ${this.convertExpressionToJinja(d.raw, d.valueParsed)} %}`,
|
|
951
974
|
)
|
|
952
|
-
// Names this loop binds in body scope, for the position-accurate
|
|
953
|
-
// `loopBoundNames` guard (#2488) — mirrors the ERB adapter's `loopBound`
|
|
954
|
-
// derivation, adapted to what THIS renderLoop actually binds: the
|
|
955
|
-
// for-header target(s) (`loopVar` / `objectIteration` key+value /
|
|
956
|
-
// `iterationShape === 'keys'`'s `param`), each `indexLocalLines` name
|
|
957
|
-
// (explicit index, or a destructure binding), and the `.map()`
|
|
958
|
-
// preamble's declared locals. Ref-counted so nested loops compose.
|
|
959
|
-
const loopBound: string[] = []
|
|
960
|
-
if (loop.objectIteration === 'entries') {
|
|
961
|
-
loopBound.push(loop.index ?? param, param)
|
|
962
|
-
} else if (loop.objectIteration === 'keys' || loop.objectIteration === 'values') {
|
|
963
|
-
loopBound.push(param)
|
|
964
|
-
} else if (loop.iterationShape === 'keys') {
|
|
965
|
-
// The header still binds the throwaway loop var; `param` is only the
|
|
966
|
-
// index alias set beneath it (#2488).
|
|
967
|
-
loopBound.push('__bf_item', param)
|
|
968
|
-
} else if (supportableDestructure) {
|
|
969
|
-
loopBound.push('__bf_item', ...(loop.paramBindings ?? []).map(b => b.name))
|
|
970
|
-
if (loop.index) loopBound.push(loop.index)
|
|
971
|
-
} else {
|
|
972
|
-
loopBound.push(param)
|
|
973
|
-
if (loop.index) loopBound.push(loop.index)
|
|
974
|
-
}
|
|
975
|
-
for (const d of loop.preamble?.declarations ?? []) loopBound.push(d.name)
|
|
976
|
-
for (const n of loopBound) {
|
|
977
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
978
|
-
}
|
|
979
975
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
980
|
-
for (const n of loopBound) {
|
|
981
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
982
|
-
if (c <= 0) this.loopBoundNames.delete(n)
|
|
983
|
-
else this.loopBoundNames.set(n, c)
|
|
984
|
-
}
|
|
985
976
|
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
986
977
|
this.inLoop = prevInLoop
|
|
987
978
|
void renderedChildren
|
|
@@ -989,11 +980,13 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
989
980
|
// Whole-item conditional: prepend an always-present `<!--bf-loop-i:KEY-->`
|
|
990
981
|
// anchor before each item's (possibly empty) conditional content so the
|
|
991
982
|
// client's `mapArrayAnchored` can hydrate every SSR-rendered item by its
|
|
992
|
-
// anchor.
|
|
983
|
+
// anchor. Still under the row scope (see above) — `loop.key` is a
|
|
984
|
+
// per-row expression.
|
|
993
985
|
const bodyChildren =
|
|
994
986
|
loop.bodyIsItemConditional && loop.key
|
|
995
987
|
? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}\n${childrenUnderLoop}`
|
|
996
988
|
: childrenUnderLoop
|
|
989
|
+
this.scope = prevScope
|
|
997
990
|
|
|
998
991
|
const lines: string[] = []
|
|
999
992
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
@@ -1472,13 +1465,13 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1472
1465
|
// function-scope (`!isModule`) consts whose value is NOT itself a bare
|
|
1473
1466
|
// identifier (loop guard) are considered.
|
|
1474
1467
|
//
|
|
1475
|
-
// `
|
|
1476
|
-
// param can shadow this outer const's name (`.map((attrs) => <p
|
|
1468
|
+
// `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
|
|
1469
|
+
// own param can shadow this outer const's name (`.map((attrs) => <p
|
|
1477
1470
|
// {...attrs} />)`) — without the guard this forwarded the OUTER
|
|
1478
1471
|
// const's value at every iteration instead of the per-item value.
|
|
1479
|
-
// Must be the
|
|
1480
|
-
//
|
|
1481
|
-
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.
|
|
1472
|
+
// Must be the threaded, position-accurate scope — the same name
|
|
1473
|
+
// spread at ROOT (no enclosing loop) must still resolve the const.
|
|
1474
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
|
|
1482
1475
|
const localConst = (this.localConstants ?? []).find(
|
|
1483
1476
|
c => c.name === trimmed && !c.isModule,
|
|
1484
1477
|
)
|
|
@@ -1894,9 +1887,9 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1894
1887
|
return this.booleanTypedProps.has(bare)
|
|
1895
1888
|
}
|
|
1896
1889
|
|
|
1897
|
-
/** Position-accurate loop-bound-name check — see `
|
|
1890
|
+
/** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */
|
|
1898
1891
|
private isLoopBoundName(name: string): boolean {
|
|
1899
|
-
return this.
|
|
1892
|
+
return this.scope.isBound(name)
|
|
1900
1893
|
}
|
|
1901
1894
|
|
|
1902
1895
|
/**
|
|
@@ -1923,17 +1916,16 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1923
1916
|
* context, so a bare reference would resolve to Undefined.
|
|
1924
1917
|
*
|
|
1925
1918
|
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1926
|
-
* name
|
|
1927
|
-
* inlines (#2221) — the occurrence may be the
|
|
1928
|
-
* binding, and substituting the outer const's
|
|
1929
|
-
* iteration with the same hard-coded literal.
|
|
1930
|
-
*
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
* `collectStringValueNames`.
|
|
1919
|
+
* name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/
|
|
1920
|
+
* preamble param never inlines (#2221) — the occurrence may be the
|
|
1921
|
+
* loop's own (shadowing) binding, and substituting the outer const's
|
|
1922
|
+
* value there renders every iteration with the same hard-coded literal.
|
|
1923
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a
|
|
1924
|
+
* same-named const elsewhere in the component, outside any loop that
|
|
1925
|
+
* shadows it, still inlines.
|
|
1934
1926
|
*/
|
|
1935
1927
|
private _resolveLiteralConst(name: string): string | null {
|
|
1936
|
-
if (this.
|
|
1928
|
+
if (this.scope.isBound(name)) return null
|
|
1937
1929
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1938
1930
|
if (c?.value === undefined) return null
|
|
1939
1931
|
const v = c.value.trim()
|
|
@@ -1951,15 +1943,16 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1951
1943
|
* scope, so an enclosing loop callback's own param of the same name
|
|
1952
1944
|
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
1953
1945
|
* still resolved to the OUTER const's member value at every iteration
|
|
1954
|
-
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1946
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
|
|
1947
|
+
* Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed
|
|
1948
|
+
* as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY
|
|
1949
|
+
* ENCLOSING loop binds never inlines, falling back to the bare
|
|
1957
1950
|
* `cfg['x']` member expression (which a Jinja for-loop binds correctly
|
|
1958
|
-
* at the shadowed
|
|
1951
|
+
* at the shadowed occurrence); a same-named const elsewhere, outside any
|
|
1952
|
+
* loop that shadows it, still inlines.
|
|
1959
1953
|
*/
|
|
1960
1954
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1961
|
-
|
|
1962
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1955
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
|
|
1963
1956
|
if (!hit) return null
|
|
1964
1957
|
return hit.kind === 'number'
|
|
1965
1958
|
? hit.text
|