@barefootjs/jinja 0.18.5 → 0.18.7
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 +73 -5
- package/dist/adapter/jinja-adapter.d.ts +41 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/adapter/lib/static-value.d.ts +15 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/build.js +73 -5
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +74 -11
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/jinja-adapter-unit.test.ts +120 -0
- package/src/adapter/jinja-adapter.ts +102 -5
- package/src/adapter/lib/static-value.ts +41 -0
- package/src/conformance-pins.ts +24 -25
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +13 -142
package/dist/adapter/index.js
CHANGED
|
@@ -187308,7 +187308,13 @@ import {
|
|
|
187308
187308
|
prepareLoweringMatchers,
|
|
187309
187309
|
queryHrefArgs,
|
|
187310
187310
|
isValidHelperId,
|
|
187311
|
-
sortComparatorFromArrow as sortComparatorFromArrow2
|
|
187311
|
+
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
187312
|
+
isDangerousInnerHtmlAttr,
|
|
187313
|
+
resolveDangerousInnerHtml,
|
|
187314
|
+
dangerousInnerHtmlMetacharViolation,
|
|
187315
|
+
dangerousInnerHtmlDiagnostic,
|
|
187316
|
+
resolveStaticLoopSource,
|
|
187317
|
+
collectLoopBoundNames
|
|
187312
187318
|
} from "@barefootjs/jsx";
|
|
187313
187319
|
|
|
187314
187320
|
// src/adapter/boolean-result.ts
|
|
@@ -187699,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187699
187705
|
return `bf.flat(${recv}, ${d})`;
|
|
187700
187706
|
}
|
|
187701
187707
|
|
|
187708
|
+
// src/adapter/lib/static-value.ts
|
|
187709
|
+
function staticValueToJinja(value) {
|
|
187710
|
+
if (value === null || value === undefined)
|
|
187711
|
+
return "none";
|
|
187712
|
+
if (typeof value === "boolean")
|
|
187713
|
+
return value ? "true" : "false";
|
|
187714
|
+
if (typeof value === "number")
|
|
187715
|
+
return String(value);
|
|
187716
|
+
if (typeof value === "string")
|
|
187717
|
+
return `'${escapeJinjaSingleQuoted(value)}'`;
|
|
187718
|
+
if (Array.isArray(value)) {
|
|
187719
|
+
const items = [];
|
|
187720
|
+
for (const el of value) {
|
|
187721
|
+
const serialized = staticValueToJinja(el);
|
|
187722
|
+
if (serialized === null)
|
|
187723
|
+
return null;
|
|
187724
|
+
items.push(serialized);
|
|
187725
|
+
}
|
|
187726
|
+
return `[${items.join(", ")}]`;
|
|
187727
|
+
}
|
|
187728
|
+
if (typeof value === "object") {
|
|
187729
|
+
const entries = [];
|
|
187730
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187731
|
+
const serialized = staticValueToJinja(val);
|
|
187732
|
+
if (serialized === null)
|
|
187733
|
+
return null;
|
|
187734
|
+
entries.push(`${jinjaHashKey(key)}: ${serialized}`);
|
|
187735
|
+
}
|
|
187736
|
+
return `{${entries.join(", ")}}`;
|
|
187737
|
+
}
|
|
187738
|
+
return null;
|
|
187739
|
+
}
|
|
187740
|
+
|
|
187702
187741
|
// src/adapter/expr/emitters.ts
|
|
187703
187742
|
import {
|
|
187704
187743
|
groupBinaryOperand,
|
|
@@ -188281,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188281
188320
|
_searchParamsLocals = new Set;
|
|
188282
188321
|
_loweringMatchers = [];
|
|
188283
188322
|
localConstants = [];
|
|
188323
|
+
staticLoopSourceBoundNames = new Set;
|
|
188284
188324
|
nullableOptionalProps = new Set;
|
|
188285
188325
|
constructor(options = {}) {
|
|
188286
188326
|
super();
|
|
@@ -188296,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188296
188336
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188297
188337
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188298
188338
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188339
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188299
188340
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188300
188341
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188301
188342
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188417,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188417
188458
|
renderElement(element) {
|
|
188418
188459
|
const tag = element.tag;
|
|
188419
188460
|
const attrs = this.renderAttributes(element);
|
|
188420
|
-
const
|
|
188461
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188462
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188421
188463
|
let hydrationAttrs = "";
|
|
188422
188464
|
if (element.needsScope) {
|
|
188423
188465
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188452,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188452
188494
|
}
|
|
188453
188495
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188454
188496
|
}
|
|
188497
|
+
renderDangerousInnerHtml(element) {
|
|
188498
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188499
|
+
if (!resolution)
|
|
188500
|
+
return null;
|
|
188501
|
+
if (resolution.kind === "dynamic") {
|
|
188502
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188503
|
+
return "";
|
|
188504
|
+
}
|
|
188505
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188506
|
+
if (violation) {
|
|
188507
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188508
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188509
|
+
return "";
|
|
188510
|
+
}
|
|
188511
|
+
return resolution.html;
|
|
188512
|
+
}
|
|
188455
188513
|
renderExpression(expr) {
|
|
188456
188514
|
if (expr.clientOnly) {
|
|
188457
188515
|
if (expr.slotId) {
|
|
@@ -188459,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188459
188517
|
}
|
|
188460
188518
|
return "";
|
|
188461
188519
|
}
|
|
188462
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188520
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188463
188521
|
if (expr.slotId) {
|
|
188464
188522
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188465
188523
|
}
|
|
@@ -188551,8 +188609,12 @@ ${whenTrue}
|
|
|
188551
188609
|
}
|
|
188552
188610
|
});
|
|
188553
188611
|
}
|
|
188612
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188613
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188614
|
+
});
|
|
188615
|
+
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
|
|
188554
188616
|
const arrayName = loop.array.trim();
|
|
188555
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188617
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188556
188618
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188557
188619
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188558
188620
|
this.errors.push({
|
|
@@ -188566,7 +188628,7 @@ ${whenTrue}
|
|
|
188566
188628
|
});
|
|
188567
188629
|
}
|
|
188568
188630
|
}
|
|
188569
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188631
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188570
188632
|
let array = rawArray;
|
|
188571
188633
|
if (loop.sortComparator) {
|
|
188572
188634
|
const sort = loop.sortComparator;
|
|
@@ -188878,6 +188940,8 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188878
188940
|
for (const attr of element.attrs) {
|
|
188879
188941
|
if (attr.clientOnly)
|
|
188880
188942
|
continue;
|
|
188943
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188944
|
+
continue;
|
|
188881
188945
|
let attrName;
|
|
188882
188946
|
if (attr.name === "className")
|
|
188883
188947
|
attrName = "class";
|
|
@@ -189077,6 +189141,8 @@ Options:
|
|
|
189077
189141
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189078
189142
|
}
|
|
189079
189143
|
_resolveLiteralConst(name) {
|
|
189144
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189145
|
+
return null;
|
|
189080
189146
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189081
189147
|
if (c?.value === undefined)
|
|
189082
189148
|
return null;
|
|
@@ -189089,6 +189155,8 @@ Options:
|
|
|
189089
189155
|
return null;
|
|
189090
189156
|
}
|
|
189091
189157
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189158
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189159
|
+
return null;
|
|
189092
189160
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189093
189161
|
if (!hit)
|
|
189094
189162
|
return null;
|
|
@@ -168,6 +168,16 @@ export declare class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<J
|
|
|
168
168
|
* const (`const sizeAttrs = size ? {…} : {}`) to its initializer text.
|
|
169
169
|
*/
|
|
170
170
|
private localConstants;
|
|
171
|
+
/**
|
|
172
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
173
|
+
* parameter anywhere in the component (#2208 fable review). A static
|
|
174
|
+
* loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
|
|
175
|
+
* never resolve through `resolveStaticLoopSource` at a use site where a
|
|
176
|
+
* DIFFERENT, enclosing loop's own callback param shadows it — same
|
|
177
|
+
* shadowing hazard, and same coarse-but-safe mitigation, as #2212's
|
|
178
|
+
* `collectLoopBoundNames` use in `collectStringValueNames`.
|
|
179
|
+
*/
|
|
180
|
+
private staticLoopSourceBoundNames;
|
|
171
181
|
/**
|
|
172
182
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
173
183
|
* Their bare-reference attribute emission is guarded with a Jinja
|
|
@@ -219,6 +229,13 @@ export declare class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<J
|
|
|
219
229
|
private providerObjectLiteralJinja;
|
|
220
230
|
emitAsync(node: IRAsync, _ctx: JinjaRenderCtx, _emit: EmitIRNode<JinjaRenderCtx>): string;
|
|
221
231
|
renderElement(element: IRElement): string;
|
|
232
|
+
/**
|
|
233
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
234
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
235
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
236
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
237
|
+
*/
|
|
238
|
+
private renderDangerousInnerHtml;
|
|
222
239
|
renderExpression(expr: IRExpression): string;
|
|
223
240
|
renderConditional(cond: IRConditional): string;
|
|
224
241
|
private renderNodeOrNull;
|
|
@@ -390,8 +407,32 @@ export declare class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<J
|
|
|
390
407
|
* single-quoted string literal (`const totalPages = 5`, #1897
|
|
391
408
|
* pagination) — function-scope consts never reach the per-render
|
|
392
409
|
* context, so a bare reference would resolve to Undefined.
|
|
410
|
+
*
|
|
411
|
+
* The lookup is a flat name match with no notion of AST scope, so a
|
|
412
|
+
* name that any loop callback binds as its item/index param never
|
|
413
|
+
* inlines (#2221) — the occurrence may be the loop's own (shadowing)
|
|
414
|
+
* binding, and substituting the outer const's value there renders every
|
|
415
|
+
* iteration with the same hard-coded literal. Coarse (a genuinely
|
|
416
|
+
* non-shadowed same-named const elsewhere in the component also stops
|
|
417
|
+
* inlining, falling back to the bare identifier) but safe — the same
|
|
418
|
+
* trade-off as #2212's `collectLoopBoundNames` use in
|
|
419
|
+
* `collectStringValueNames`.
|
|
393
420
|
*/
|
|
394
421
|
private _resolveLiteralConst;
|
|
422
|
+
/**
|
|
423
|
+
* Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
|
|
424
|
+
* (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
|
|
425
|
+
*
|
|
426
|
+
* The lookup is a flat name match on `objectName` with no notion of AST
|
|
427
|
+
* scope, so an enclosing loop callback's own param of the same name
|
|
428
|
+
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
429
|
+
* still resolved to the OUTER const's member value at every iteration
|
|
430
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
|
|
431
|
+
* coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
|
|
432
|
+
* binds anywhere in the component never inlines, falling back to the bare
|
|
433
|
+
* `cfg['x']` member expression (which a Jinja for-loop binds correctly
|
|
434
|
+
* at the shadowed occurrences).
|
|
435
|
+
*/
|
|
395
436
|
private _resolveStaticRecordLiteral;
|
|
396
437
|
private _resolveModuleStringConst;
|
|
397
438
|
private _recordExprBF101;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/jinja-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAKP,yBAAyB,EAE1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAE3B,KAAK,aAAa,EAClB,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"jinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/jinja-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Jinja2 literal. Used to inline a fully-static loop source (an inline
|
|
5
|
+
* array literal, or a function-scope local const with a static
|
|
6
|
+
* initializer) directly in a `{% for %}` header, rather than requiring a
|
|
7
|
+
* bound template variable.
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` for a value this adapter can't represent as a literal
|
|
10
|
+
* (e.g. `undefined` reads for a missing object key are still representable
|
|
11
|
+
* as `none`, but anything else falls back to the caller's existing BF101
|
|
12
|
+
* refusal instead of guessing).
|
|
13
|
+
*/
|
|
14
|
+
export declare function staticValueToJinja(value: unknown): string | null;
|
|
15
|
+
//# sourceMappingURL=static-value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-value.d.ts","sourceRoot":"","sources":["../../../src/adapter/lib/static-value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwBhE"}
|
package/dist/build.js
CHANGED
|
@@ -187308,7 +187308,13 @@ import {
|
|
|
187308
187308
|
prepareLoweringMatchers,
|
|
187309
187309
|
queryHrefArgs,
|
|
187310
187310
|
isValidHelperId,
|
|
187311
|
-
sortComparatorFromArrow as sortComparatorFromArrow2
|
|
187311
|
+
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
187312
|
+
isDangerousInnerHtmlAttr,
|
|
187313
|
+
resolveDangerousInnerHtml,
|
|
187314
|
+
dangerousInnerHtmlMetacharViolation,
|
|
187315
|
+
dangerousInnerHtmlDiagnostic,
|
|
187316
|
+
resolveStaticLoopSource,
|
|
187317
|
+
collectLoopBoundNames
|
|
187312
187318
|
} from "@barefootjs/jsx";
|
|
187313
187319
|
|
|
187314
187320
|
// src/adapter/boolean-result.ts
|
|
@@ -187699,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187699
187705
|
return `bf.flat(${recv}, ${d})`;
|
|
187700
187706
|
}
|
|
187701
187707
|
|
|
187708
|
+
// src/adapter/lib/static-value.ts
|
|
187709
|
+
function staticValueToJinja(value) {
|
|
187710
|
+
if (value === null || value === undefined)
|
|
187711
|
+
return "none";
|
|
187712
|
+
if (typeof value === "boolean")
|
|
187713
|
+
return value ? "true" : "false";
|
|
187714
|
+
if (typeof value === "number")
|
|
187715
|
+
return String(value);
|
|
187716
|
+
if (typeof value === "string")
|
|
187717
|
+
return `'${escapeJinjaSingleQuoted(value)}'`;
|
|
187718
|
+
if (Array.isArray(value)) {
|
|
187719
|
+
const items = [];
|
|
187720
|
+
for (const el of value) {
|
|
187721
|
+
const serialized = staticValueToJinja(el);
|
|
187722
|
+
if (serialized === null)
|
|
187723
|
+
return null;
|
|
187724
|
+
items.push(serialized);
|
|
187725
|
+
}
|
|
187726
|
+
return `[${items.join(", ")}]`;
|
|
187727
|
+
}
|
|
187728
|
+
if (typeof value === "object") {
|
|
187729
|
+
const entries = [];
|
|
187730
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187731
|
+
const serialized = staticValueToJinja(val);
|
|
187732
|
+
if (serialized === null)
|
|
187733
|
+
return null;
|
|
187734
|
+
entries.push(`${jinjaHashKey(key)}: ${serialized}`);
|
|
187735
|
+
}
|
|
187736
|
+
return `{${entries.join(", ")}}`;
|
|
187737
|
+
}
|
|
187738
|
+
return null;
|
|
187739
|
+
}
|
|
187740
|
+
|
|
187702
187741
|
// src/adapter/expr/emitters.ts
|
|
187703
187742
|
import {
|
|
187704
187743
|
groupBinaryOperand,
|
|
@@ -188281,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188281
188320
|
_searchParamsLocals = new Set;
|
|
188282
188321
|
_loweringMatchers = [];
|
|
188283
188322
|
localConstants = [];
|
|
188323
|
+
staticLoopSourceBoundNames = new Set;
|
|
188284
188324
|
nullableOptionalProps = new Set;
|
|
188285
188325
|
constructor(options = {}) {
|
|
188286
188326
|
super();
|
|
@@ -188296,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188296
188336
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188297
188337
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188298
188338
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188339
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188299
188340
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188300
188341
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188301
188342
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188417,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188417
188458
|
renderElement(element) {
|
|
188418
188459
|
const tag = element.tag;
|
|
188419
188460
|
const attrs = this.renderAttributes(element);
|
|
188420
|
-
const
|
|
188461
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188462
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188421
188463
|
let hydrationAttrs = "";
|
|
188422
188464
|
if (element.needsScope) {
|
|
188423
188465
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188452,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188452
188494
|
}
|
|
188453
188495
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188454
188496
|
}
|
|
188497
|
+
renderDangerousInnerHtml(element) {
|
|
188498
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188499
|
+
if (!resolution)
|
|
188500
|
+
return null;
|
|
188501
|
+
if (resolution.kind === "dynamic") {
|
|
188502
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188503
|
+
return "";
|
|
188504
|
+
}
|
|
188505
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188506
|
+
if (violation) {
|
|
188507
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188508
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188509
|
+
return "";
|
|
188510
|
+
}
|
|
188511
|
+
return resolution.html;
|
|
188512
|
+
}
|
|
188455
188513
|
renderExpression(expr) {
|
|
188456
188514
|
if (expr.clientOnly) {
|
|
188457
188515
|
if (expr.slotId) {
|
|
@@ -188459,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188459
188517
|
}
|
|
188460
188518
|
return "";
|
|
188461
188519
|
}
|
|
188462
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188520
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188463
188521
|
if (expr.slotId) {
|
|
188464
188522
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188465
188523
|
}
|
|
@@ -188551,8 +188609,12 @@ ${whenTrue}
|
|
|
188551
188609
|
}
|
|
188552
188610
|
});
|
|
188553
188611
|
}
|
|
188612
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188613
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188614
|
+
});
|
|
188615
|
+
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
|
|
188554
188616
|
const arrayName = loop.array.trim();
|
|
188555
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188617
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188556
188618
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188557
188619
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188558
188620
|
this.errors.push({
|
|
@@ -188566,7 +188628,7 @@ ${whenTrue}
|
|
|
188566
188628
|
});
|
|
188567
188629
|
}
|
|
188568
188630
|
}
|
|
188569
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188631
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188570
188632
|
let array = rawArray;
|
|
188571
188633
|
if (loop.sortComparator) {
|
|
188572
188634
|
const sort = loop.sortComparator;
|
|
@@ -188878,6 +188940,8 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188878
188940
|
for (const attr of element.attrs) {
|
|
188879
188941
|
if (attr.clientOnly)
|
|
188880
188942
|
continue;
|
|
188943
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188944
|
+
continue;
|
|
188881
188945
|
let attrName;
|
|
188882
188946
|
if (attr.name === "className")
|
|
188883
188947
|
attrName = "class";
|
|
@@ -189077,6 +189141,8 @@ Options:
|
|
|
189077
189141
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189078
189142
|
}
|
|
189079
189143
|
_resolveLiteralConst(name) {
|
|
189144
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189145
|
+
return null;
|
|
189080
189146
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189081
189147
|
if (c?.value === undefined)
|
|
189082
189148
|
return null;
|
|
@@ -189089,6 +189155,8 @@ Options:
|
|
|
189089
189155
|
return null;
|
|
189090
189156
|
}
|
|
189091
189157
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189158
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189159
|
+
return null;
|
|
189092
189160
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189093
189161
|
if (!hit)
|
|
189094
189162
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eA6F7B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -187308,7 +187308,13 @@ import {
|
|
|
187308
187308
|
prepareLoweringMatchers,
|
|
187309
187309
|
queryHrefArgs,
|
|
187310
187310
|
isValidHelperId,
|
|
187311
|
-
sortComparatorFromArrow as sortComparatorFromArrow2
|
|
187311
|
+
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
187312
|
+
isDangerousInnerHtmlAttr,
|
|
187313
|
+
resolveDangerousInnerHtml,
|
|
187314
|
+
dangerousInnerHtmlMetacharViolation,
|
|
187315
|
+
dangerousInnerHtmlDiagnostic,
|
|
187316
|
+
resolveStaticLoopSource,
|
|
187317
|
+
collectLoopBoundNames
|
|
187312
187318
|
} from "@barefootjs/jsx";
|
|
187313
187319
|
|
|
187314
187320
|
// src/adapter/boolean-result.ts
|
|
@@ -187699,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187699
187705
|
return `bf.flat(${recv}, ${d})`;
|
|
187700
187706
|
}
|
|
187701
187707
|
|
|
187708
|
+
// src/adapter/lib/static-value.ts
|
|
187709
|
+
function staticValueToJinja(value) {
|
|
187710
|
+
if (value === null || value === undefined)
|
|
187711
|
+
return "none";
|
|
187712
|
+
if (typeof value === "boolean")
|
|
187713
|
+
return value ? "true" : "false";
|
|
187714
|
+
if (typeof value === "number")
|
|
187715
|
+
return String(value);
|
|
187716
|
+
if (typeof value === "string")
|
|
187717
|
+
return `'${escapeJinjaSingleQuoted(value)}'`;
|
|
187718
|
+
if (Array.isArray(value)) {
|
|
187719
|
+
const items = [];
|
|
187720
|
+
for (const el of value) {
|
|
187721
|
+
const serialized = staticValueToJinja(el);
|
|
187722
|
+
if (serialized === null)
|
|
187723
|
+
return null;
|
|
187724
|
+
items.push(serialized);
|
|
187725
|
+
}
|
|
187726
|
+
return `[${items.join(", ")}]`;
|
|
187727
|
+
}
|
|
187728
|
+
if (typeof value === "object") {
|
|
187729
|
+
const entries = [];
|
|
187730
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187731
|
+
const serialized = staticValueToJinja(val);
|
|
187732
|
+
if (serialized === null)
|
|
187733
|
+
return null;
|
|
187734
|
+
entries.push(`${jinjaHashKey(key)}: ${serialized}`);
|
|
187735
|
+
}
|
|
187736
|
+
return `{${entries.join(", ")}}`;
|
|
187737
|
+
}
|
|
187738
|
+
return null;
|
|
187739
|
+
}
|
|
187740
|
+
|
|
187702
187741
|
// src/adapter/expr/emitters.ts
|
|
187703
187742
|
import {
|
|
187704
187743
|
groupBinaryOperand,
|
|
@@ -188281,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188281
188320
|
_searchParamsLocals = new Set;
|
|
188282
188321
|
_loweringMatchers = [];
|
|
188283
188322
|
localConstants = [];
|
|
188323
|
+
staticLoopSourceBoundNames = new Set;
|
|
188284
188324
|
nullableOptionalProps = new Set;
|
|
188285
188325
|
constructor(options = {}) {
|
|
188286
188326
|
super();
|
|
@@ -188296,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188296
188336
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188297
188337
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188298
188338
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188339
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188299
188340
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188300
188341
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188301
188342
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188417,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188417
188458
|
renderElement(element) {
|
|
188418
188459
|
const tag = element.tag;
|
|
188419
188460
|
const attrs = this.renderAttributes(element);
|
|
188420
|
-
const
|
|
188461
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188462
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188421
188463
|
let hydrationAttrs = "";
|
|
188422
188464
|
if (element.needsScope) {
|
|
188423
188465
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188452,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188452
188494
|
}
|
|
188453
188495
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188454
188496
|
}
|
|
188497
|
+
renderDangerousInnerHtml(element) {
|
|
188498
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188499
|
+
if (!resolution)
|
|
188500
|
+
return null;
|
|
188501
|
+
if (resolution.kind === "dynamic") {
|
|
188502
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188503
|
+
return "";
|
|
188504
|
+
}
|
|
188505
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188506
|
+
if (violation) {
|
|
188507
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188508
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188509
|
+
return "";
|
|
188510
|
+
}
|
|
188511
|
+
return resolution.html;
|
|
188512
|
+
}
|
|
188455
188513
|
renderExpression(expr) {
|
|
188456
188514
|
if (expr.clientOnly) {
|
|
188457
188515
|
if (expr.slotId) {
|
|
@@ -188459,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188459
188517
|
}
|
|
188460
188518
|
return "";
|
|
188461
188519
|
}
|
|
188462
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188520
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188463
188521
|
if (expr.slotId) {
|
|
188464
188522
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188465
188523
|
}
|
|
@@ -188551,8 +188609,12 @@ ${whenTrue}
|
|
|
188551
188609
|
}
|
|
188552
188610
|
});
|
|
188553
188611
|
}
|
|
188612
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188613
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188614
|
+
});
|
|
188615
|
+
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
|
|
188554
188616
|
const arrayName = loop.array.trim();
|
|
188555
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188617
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188556
188618
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188557
188619
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188558
188620
|
this.errors.push({
|
|
@@ -188566,7 +188628,7 @@ ${whenTrue}
|
|
|
188566
188628
|
});
|
|
188567
188629
|
}
|
|
188568
188630
|
}
|
|
188569
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188631
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188570
188632
|
let array = rawArray;
|
|
188571
188633
|
if (loop.sortComparator) {
|
|
188572
188634
|
const sort = loop.sortComparator;
|
|
@@ -188878,6 +188940,8 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188878
188940
|
for (const attr of element.attrs) {
|
|
188879
188941
|
if (attr.clientOnly)
|
|
188880
188942
|
continue;
|
|
188943
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188944
|
+
continue;
|
|
188881
188945
|
let attrName;
|
|
188882
188946
|
if (attr.name === "className")
|
|
188883
188947
|
attrName = "class";
|
|
@@ -189077,6 +189141,8 @@ Options:
|
|
|
189077
189141
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189078
189142
|
}
|
|
189079
189143
|
_resolveLiteralConst(name) {
|
|
189144
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189145
|
+
return null;
|
|
189080
189146
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189081
189147
|
if (c?.value === undefined)
|
|
189082
189148
|
return null;
|
|
@@ -189089,6 +189155,8 @@ Options:
|
|
|
189089
189155
|
return null;
|
|
189090
189156
|
}
|
|
189091
189157
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189158
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189159
|
+
return null;
|
|
189092
189160
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189093
189161
|
if (!hit)
|
|
189094
189162
|
return null;
|
|
@@ -189126,14 +189194,10 @@ Options:
|
|
|
189126
189194
|
var jinjaAdapter = new JinjaAdapter;
|
|
189127
189195
|
// src/conformance-pins.ts
|
|
189128
189196
|
var conformancePins = {
|
|
189129
|
-
"static-array-children": [{ code: "BF103", severity: "error" }],
|
|
189130
|
-
"todo-app": [{ code: "BF103", severity: "error" }],
|
|
189131
|
-
"todo-app-ssr": [{ code: "BF103", severity: "error" }],
|
|
189132
189197
|
"static-array-from-props": [
|
|
189133
189198
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189134
189199
|
],
|
|
189135
189200
|
"static-array-from-props-with-component": [
|
|
189136
|
-
{ code: "BF103", severity: "error" },
|
|
189137
189201
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189138
189202
|
],
|
|
189139
189203
|
"filter-nested-callback-predicate": [
|
|
@@ -189142,8 +189206,7 @@ var conformancePins = {
|
|
|
189142
189206
|
"filter-nested-find-predicate": [
|
|
189143
189207
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189144
189208
|
],
|
|
189145
|
-
"
|
|
189146
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189209
|
+
"dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
|
|
189147
189210
|
};
|
|
189148
189211
|
// src/render-divergences.ts
|
|
189149
189212
|
var renderDivergences = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAK/B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jinja",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
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,14 +53,14 @@
|
|
|
53
53
|
"directory": "packages/adapter-jinja"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.18.
|
|
56
|
+
"@barefootjs/shared": "0.18.7"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/jsx": ">=0.2.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
63
|
-
"@barefootjs/jsx": "0.18.
|
|
63
|
+
"@barefootjs/jsx": "0.18.7",
|
|
64
64
|
"typescript": "^5.0.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -409,3 +409,123 @@ export function Parent() {
|
|
|
409
409
|
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and
|
|
410
410
|
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
411
411
|
// twin, which must render clean).
|
|
412
|
+
|
|
413
|
+
// #2221: `_resolveLiteralConst` is a flat name lookup against
|
|
414
|
+
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
415
|
+
// substitute an outer const's literal value even at an occurrence that is
|
|
416
|
+
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
417
|
+
// iteration rendered the same hard-coded literal. Guarded with the same
|
|
418
|
+
// coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
|
|
419
|
+
// anywhere in the component never inlines, falling back to the bare
|
|
420
|
+
// identifier. SSR-only tests for the same #2222 reason as the #2212
|
|
421
|
+
// describe below.
|
|
422
|
+
describe('JinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
423
|
+
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
424
|
+
const { template } = compileAndGenerate(`
|
|
425
|
+
function Widget() {
|
|
426
|
+
const label: string = 'x'
|
|
427
|
+
return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
428
|
+
}
|
|
429
|
+
`)
|
|
430
|
+
// The loop body must reference the per-iteration loop var...
|
|
431
|
+
expect(template).toContain('1 + label')
|
|
432
|
+
// ...never the outer const's hard-coded value.
|
|
433
|
+
expect(template).not.toContain("1 + 'x'")
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('a numeric const shadowed by a loop param emits the identifier too', () => {
|
|
437
|
+
const { template } = compileAndGenerate(`
|
|
438
|
+
function Widget() {
|
|
439
|
+
const count = 7
|
|
440
|
+
return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
|
|
441
|
+
}
|
|
442
|
+
`)
|
|
443
|
+
expect(template).toContain('1 + count')
|
|
444
|
+
expect(template).not.toContain('1 + 7')
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
|
|
448
|
+
const { template } = compileAndGenerate(`
|
|
449
|
+
function Widget({ values }: { values: number[] }) {
|
|
450
|
+
const totalPages = 5
|
|
451
|
+
return <div>
|
|
452
|
+
<p>Page 1 of {1 + totalPages}</p>
|
|
453
|
+
<ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
|
|
454
|
+
</div>
|
|
455
|
+
}
|
|
456
|
+
`)
|
|
457
|
+
expect(template).toContain('1 + 5')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// The accepted coarse-exclusion trade-off (same as #2212): a name that is
|
|
461
|
+
// loop-bound ANYWHERE in the component never inlines, even at a genuinely
|
|
462
|
+
// non-shadowed occurrence outside the loop — the bare identifier is
|
|
463
|
+
// emitted instead of the value.
|
|
464
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
|
|
465
|
+
const { template } = compileAndGenerate(`
|
|
466
|
+
function Widget({ values }: { values: number[] }) {
|
|
467
|
+
const label: string = 'x'
|
|
468
|
+
return <div>
|
|
469
|
+
<p>{1 + label}</p>
|
|
470
|
+
<ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
|
|
471
|
+
</div>
|
|
472
|
+
}
|
|
473
|
+
`)
|
|
474
|
+
expect(template).not.toContain("1 + 'x'")
|
|
475
|
+
expect(template).toContain('2 + label')
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
// #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope
|
|
480
|
+
// object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a
|
|
481
|
+
// flat name lookup on `objectName` with no notion of AST scope, the
|
|
482
|
+
// record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to
|
|
483
|
+
// substitute the outer const's member value even at an occurrence that is
|
|
484
|
+
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
485
|
+
// iteration rendered the same hard-coded literal instead of the per-item
|
|
486
|
+
// value. Guarded with the same coarse `staticLoopSourceBoundNames`
|
|
487
|
+
// exclusion as #2221: any name a loop binds anywhere in the component
|
|
488
|
+
// never inlines, falling back to the bare `cfg['x']` member expression.
|
|
489
|
+
describe('JinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
490
|
+
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
491
|
+
const { template } = compileAndGenerate(`
|
|
492
|
+
const cfg = { x: 'outer-lit' }
|
|
493
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
494
|
+
return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
495
|
+
}
|
|
496
|
+
`)
|
|
497
|
+
// The loop body must reference the per-iteration member access...
|
|
498
|
+
expect(template).toContain("bf.string(cfg['x'])")
|
|
499
|
+
// ...never the outer const's hard-coded value.
|
|
500
|
+
expect(template).not.toContain("bf.string('outer-lit')")
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
|
|
504
|
+
const { template } = compileAndGenerate(`
|
|
505
|
+
const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
|
|
506
|
+
function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
507
|
+
return <div>{variantClasses.ghost}</div>
|
|
508
|
+
}
|
|
509
|
+
`)
|
|
510
|
+
expect(template).toContain("bf.string('bg-ghost')")
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// The accepted coarse-exclusion trade-off (same as #2221/#2212): an
|
|
514
|
+
// object name that is loop-bound ANYWHERE in the component never
|
|
515
|
+
// inlines its member lookups, even at a genuinely non-shadowed
|
|
516
|
+
// occurrence outside the loop — the bare member expression is emitted
|
|
517
|
+
// instead of the value.
|
|
518
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
|
|
519
|
+
const { template } = compileAndGenerate(`
|
|
520
|
+
const cfg = { x: 'outer-lit' }
|
|
521
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
522
|
+
return <div>
|
|
523
|
+
<p>{cfg.x}</p>
|
|
524
|
+
<ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
525
|
+
</div>
|
|
526
|
+
}
|
|
527
|
+
`)
|
|
528
|
+
expect(template).not.toContain("bf.string('outer-lit')")
|
|
529
|
+
expect(template).toContain("bf.string(cfg['x'])")
|
|
530
|
+
})
|
|
531
|
+
})
|
|
@@ -140,6 +140,12 @@ import {
|
|
|
140
140
|
queryHrefArgs,
|
|
141
141
|
isValidHelperId,
|
|
142
142
|
sortComparatorFromArrow,
|
|
143
|
+
isDangerousInnerHtmlAttr,
|
|
144
|
+
resolveDangerousInnerHtml,
|
|
145
|
+
dangerousInnerHtmlMetacharViolation,
|
|
146
|
+
dangerousInnerHtmlDiagnostic,
|
|
147
|
+
resolveStaticLoopSource,
|
|
148
|
+
collectLoopBoundNames,
|
|
143
149
|
} from '@barefootjs/jsx'
|
|
144
150
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
145
151
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
@@ -158,6 +164,7 @@ import {
|
|
|
158
164
|
collectRootScopeNodes,
|
|
159
165
|
} from './lib/ir-scope.ts'
|
|
160
166
|
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
167
|
+
import { staticValueToJinja } from './lib/static-value.ts'
|
|
161
168
|
import { JinjaFilterEmitter, JinjaTopLevelEmitter, truthyTest } from './expr/emitters.ts'
|
|
162
169
|
import type { JinjaEmitContext, JinjaSpreadContext, JinjaMemoContext } from './emit-context.ts'
|
|
163
170
|
import {
|
|
@@ -266,6 +273,17 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
266
273
|
*/
|
|
267
274
|
private localConstants: IRMetadata['localConstants'] = []
|
|
268
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
278
|
+
* parameter anywhere in the component (#2208 fable review). A static
|
|
279
|
+
* loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
|
|
280
|
+
* never resolve through `resolveStaticLoopSource` at a use site where a
|
|
281
|
+
* DIFFERENT, enclosing loop's own callback param shadows it — same
|
|
282
|
+
* shadowing hazard, and same coarse-but-safe mitigation, as #2212's
|
|
283
|
+
* `collectLoopBoundNames` use in `collectStringValueNames`.
|
|
284
|
+
*/
|
|
285
|
+
private staticLoopSourceBoundNames: Set<string> = new Set()
|
|
286
|
+
|
|
269
287
|
/**
|
|
270
288
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
271
289
|
* Their bare-reference attribute emission is guarded with a Jinja
|
|
@@ -298,6 +316,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
298
316
|
// ("True"/"False") (#1897, pagination's data-active).
|
|
299
317
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
300
318
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
319
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
|
|
301
320
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
302
321
|
this.stringValueNames = collectStringValueNames(ir)
|
|
303
322
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -520,7 +539,8 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
520
539
|
renderElement(element: IRElement): string {
|
|
521
540
|
const tag = element.tag
|
|
522
541
|
const attrs = this.renderAttributes(element)
|
|
523
|
-
const
|
|
542
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
543
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
524
544
|
|
|
525
545
|
let hydrationAttrs = ''
|
|
526
546
|
if (element.needsScope) {
|
|
@@ -555,6 +575,28 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
555
575
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
556
576
|
}
|
|
557
577
|
|
|
578
|
+
/**
|
|
579
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
580
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
581
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
582
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
583
|
+
*/
|
|
584
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
585
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
586
|
+
if (!resolution) return null
|
|
587
|
+
if (resolution.kind === 'dynamic') {
|
|
588
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
589
|
+
return ''
|
|
590
|
+
}
|
|
591
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
592
|
+
if (violation) {
|
|
593
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
594
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
595
|
+
return ''
|
|
596
|
+
}
|
|
597
|
+
return resolution.html
|
|
598
|
+
}
|
|
599
|
+
|
|
558
600
|
// ===========================================================================
|
|
559
601
|
// Expression Rendering
|
|
560
602
|
// ===========================================================================
|
|
@@ -568,8 +610,12 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
568
610
|
}
|
|
569
611
|
|
|
570
612
|
// Text-position interpolation of a possibly-non-string value — see the
|
|
571
|
-
// file header, divergence 2.
|
|
572
|
-
|
|
613
|
+
// file header, divergence 2. Thread the IR-carried `.parsed` tree
|
|
614
|
+
// through (mirrors go-template's `convertExpressionToGo(expr.expr,
|
|
615
|
+
// classify, expr.parsed)`) so a resolved bare-identifier
|
|
616
|
+
// `.map`/`.filter`/… callback (`resolveCallbackMethodFunctionReferences`,
|
|
617
|
+
// #2206) isn't lost to a fresh, unresolved re-parse of the raw string.
|
|
618
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`
|
|
573
619
|
|
|
574
620
|
if (expr.slotId) {
|
|
575
621
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`
|
|
@@ -716,8 +762,27 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
716
762
|
// test corpus only because the widened destructure gate (#2087 Phase
|
|
717
763
|
// A/B) no longer refuses this fixture's `([emoji, users]) => ...`
|
|
718
764
|
// param first.
|
|
765
|
+
// #2208: a loop source that is a fully-static array literal — either
|
|
766
|
+
// inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
|
|
767
|
+
// bound to a FUNCTION-scope local const whose initializer has no
|
|
768
|
+
// prop/signal/function-call dependency — inlines as a native Jinja
|
|
769
|
+
// list/dict literal below, the same way a module-scope const's value
|
|
770
|
+
// is already seeded. A runtime-computed local (#2069, e.g.
|
|
771
|
+
// `Object.entries(props.tags).filter(...)`) still refuses below.
|
|
772
|
+
// `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
|
|
773
|
+
// param shadowing this identifier (fable review) — never resolve the
|
|
774
|
+
// static const in that case. `rawArray` then falls through to the
|
|
775
|
+
// bare identifier expression below, same as before #2208 — which
|
|
776
|
+
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
777
|
+
// const reference (a loud, conservative refusal, not a silent wrong
|
|
778
|
+
// value).
|
|
779
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
780
|
+
isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
|
|
781
|
+
})
|
|
782
|
+
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null
|
|
783
|
+
|
|
719
784
|
const arrayName = loop.array.trim()
|
|
720
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
785
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
721
786
|
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
722
787
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
723
788
|
this.errors.push({
|
|
@@ -733,7 +798,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
733
798
|
}
|
|
734
799
|
}
|
|
735
800
|
|
|
736
|
-
const rawArray = this.convertExpressionToJinja(loop.array)
|
|
801
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array)
|
|
737
802
|
// Apply sort if present: wrap the loop array in the shared `bf.sort`
|
|
738
803
|
// helper, binding the sorted result to a per-iteration local so the
|
|
739
804
|
// helper runs once.
|
|
@@ -1385,6 +1450,12 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1385
1450
|
// the unsupported-expression lowering is never reached for a deferred
|
|
1386
1451
|
// predicate (no BF101 / BF102). #1966
|
|
1387
1452
|
if (attr.clientOnly) continue
|
|
1453
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
1454
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
1455
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
1456
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
1457
|
+
// (which would double-report alongside the purpose-built one).
|
|
1458
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
1388
1459
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1389
1460
|
let attrName: string
|
|
1390
1461
|
if (attr.name === 'className') attrName = 'class'
|
|
@@ -1749,8 +1820,19 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1749
1820
|
* single-quoted string literal (`const totalPages = 5`, #1897
|
|
1750
1821
|
* pagination) — function-scope consts never reach the per-render
|
|
1751
1822
|
* context, so a bare reference would resolve to Undefined.
|
|
1823
|
+
*
|
|
1824
|
+
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1825
|
+
* name that any loop callback binds as its item/index param never
|
|
1826
|
+
* inlines (#2221) — the occurrence may be the loop's own (shadowing)
|
|
1827
|
+
* binding, and substituting the outer const's value there renders every
|
|
1828
|
+
* iteration with the same hard-coded literal. Coarse (a genuinely
|
|
1829
|
+
* non-shadowed same-named const elsewhere in the component also stops
|
|
1830
|
+
* inlining, falling back to the bare identifier) but safe — the same
|
|
1831
|
+
* trade-off as #2212's `collectLoopBoundNames` use in
|
|
1832
|
+
* `collectStringValueNames`.
|
|
1752
1833
|
*/
|
|
1753
1834
|
private _resolveLiteralConst(name: string): string | null {
|
|
1835
|
+
if (this.staticLoopSourceBoundNames.has(name)) return null
|
|
1754
1836
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1755
1837
|
if (c?.value === undefined) return null
|
|
1756
1838
|
const v = c.value.trim()
|
|
@@ -1760,7 +1842,22 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1760
1842
|
return null
|
|
1761
1843
|
}
|
|
1762
1844
|
|
|
1845
|
+
/**
|
|
1846
|
+
* Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
|
|
1847
|
+
* (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
|
|
1848
|
+
*
|
|
1849
|
+
* The lookup is a flat name match on `objectName` with no notion of AST
|
|
1850
|
+
* scope, so an enclosing loop callback's own param of the same name
|
|
1851
|
+
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
1852
|
+
* still resolved to the OUTER const's member value at every iteration
|
|
1853
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
|
|
1854
|
+
* coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
|
|
1855
|
+
* binds anywhere in the component never inlines, falling back to the bare
|
|
1856
|
+
* `cfg['x']` member expression (which a Jinja for-loop binds correctly
|
|
1857
|
+
* at the shadowed occurrences).
|
|
1858
|
+
*/
|
|
1763
1859
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1860
|
+
if (this.staticLoopSourceBoundNames.has(objectName)) return null
|
|
1764
1861
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1765
1862
|
if (!hit) return null
|
|
1766
1863
|
return hit.kind === 'number'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Jinja2 literal. Used to inline a fully-static loop source (an inline
|
|
5
|
+
* array literal, or a function-scope local const with a static
|
|
6
|
+
* initializer) directly in a `{% for %}` header, rather than requiring a
|
|
7
|
+
* bound template variable.
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` for a value this adapter can't represent as a literal
|
|
10
|
+
* (e.g. `undefined` reads for a missing object key are still representable
|
|
11
|
+
* as `none`, but anything else falls back to the caller's existing BF101
|
|
12
|
+
* refusal instead of guessing).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { escapeJinjaSingleQuoted, jinjaHashKey } from './jinja-naming.ts'
|
|
16
|
+
|
|
17
|
+
export function staticValueToJinja(value: unknown): string | null {
|
|
18
|
+
if (value === null || value === undefined) return 'none'
|
|
19
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
20
|
+
if (typeof value === 'number') return String(value)
|
|
21
|
+
if (typeof value === 'string') return `'${escapeJinjaSingleQuoted(value)}'`
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
const items: string[] = []
|
|
24
|
+
for (const el of value) {
|
|
25
|
+
const serialized = staticValueToJinja(el)
|
|
26
|
+
if (serialized === null) return null
|
|
27
|
+
items.push(serialized)
|
|
28
|
+
}
|
|
29
|
+
return `[${items.join(', ')}]`
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
const entries: string[] = []
|
|
33
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
34
|
+
const serialized = staticValueToJinja(val)
|
|
35
|
+
if (serialized === null) return null
|
|
36
|
+
entries.push(`${jinjaHashKey(key)}: ${serialized}`)
|
|
37
|
+
}
|
|
38
|
+
return `{${entries.join(', ')}}`
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
12
12
|
|
|
13
13
|
export const conformancePins: ConformancePins = {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
15
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
16
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
17
|
+
// BF103 loop-body cross-template check no longer fires spuriously. (Both
|
|
18
|
+
// fixtures are still skipped on this adapter via `render-divergences.ts`
|
|
19
|
+
// — #2209 — for an unrelated signal-seeding gap.)
|
|
20
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
21
|
+
// array-literal initializer is now recognized as fully-static
|
|
22
|
+
// (`resolveStaticLoopSource`) and inlined as a native Jinja list/dict
|
|
23
|
+
// literal in the `{% for %}` header, the same way a module-scope const's
|
|
24
|
+
// value is already seeded.
|
|
23
25
|
// #2087 Phase A/B widened the destructure gate (`isLowerableLoopDestructure`)
|
|
24
26
|
// to admit array-index / nested-path fixed bindings, so the
|
|
25
27
|
// `([emoji, users]) => ...` / `([id, t]) => ...` params in these two
|
|
@@ -44,10 +46,10 @@ export const conformancePins: ConformancePins = {
|
|
|
44
46
|
'static-array-from-props': [
|
|
45
47
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
|
|
46
48
|
],
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
+
// BF101 (unresolvable computed loop array, see above) fires; BF103
|
|
50
|
+
// (imported child in the loop body) no longer does now that the
|
|
51
|
+
// conformance harness passes `siblingTemplatesRegistered: true` (#2205).
|
|
49
52
|
'static-array-from-props-with-component': [
|
|
50
|
-
{ code: 'BF103', severity: 'error' },
|
|
51
53
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
|
|
52
54
|
],
|
|
53
55
|
// Rest-destructure / structured-path `.map()` callbacks (#2087 Phase B):
|
|
@@ -91,17 +93,14 @@ export const conformancePins: ConformancePins = {
|
|
|
91
93
|
// / etc. via the same evaluator-JSON mechanism as `.filter` / `.every` /
|
|
92
94
|
// `.some`, so they render. Only the NESTED-in-a-predicate form above is
|
|
93
95
|
// refused (#2038).
|
|
94
|
-
//
|
|
95
|
-
// callback
|
|
96
|
-
// (`
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
// the shape loudly instead of emitting entity-escaped markup that
|
|
105
|
-
// silently renders tags as text.
|
|
106
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
96
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
97
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
98
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
99
|
+
// #2090 established for `.sort(fnref)`.
|
|
100
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
101
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
102
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
103
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
104
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
105
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
107
106
|
}
|
|
@@ -14,4 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
|
-
export const renderDivergences: RenderDivergences = {
|
|
17
|
+
export const renderDivergences: RenderDivergences = {
|
|
18
|
+
// `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
|
|
19
|
+
// `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
|
|
20
|
+
// instead of a fixed regex-shape catalogue) now correctly seeds `todos`
|
|
21
|
+
// from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
|
|
22
|
+
}
|
package/src/test-render.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* generated render script (Python, not Perl) and its literal syntax differ.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
|
|
19
|
+
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
|
|
20
20
|
import type { ComponentIR } from '@barefootjs/jsx'
|
|
21
21
|
import { mkdir, rm } from 'node:fs/promises'
|
|
22
22
|
import { resolve } from 'node:path'
|
|
@@ -125,8 +125,15 @@ export async function renderJinjaComponent(options: RenderOptions): Promise<stri
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// Compile parent source.
|
|
129
|
-
|
|
128
|
+
// Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
|
|
129
|
+
// matches this harness's real behavior — every sibling child template is registered
|
|
130
|
+
// alongside the parent before rendering, so a loop-body cross-template
|
|
131
|
+
// call resolves at render time (#2205).
|
|
132
|
+
const result = compileJSX(source, 'component.tsx', {
|
|
133
|
+
adapter,
|
|
134
|
+
outputIR: true,
|
|
135
|
+
siblingTemplatesRegistered: Boolean(components),
|
|
136
|
+
})
|
|
130
137
|
|
|
131
138
|
const errors = result.errors.filter(e => e.severity === 'error')
|
|
132
139
|
if (errors.length > 0) {
|
|
@@ -461,9 +468,9 @@ function buildPythonProps(
|
|
|
461
468
|
for (const param of ir.metadata.propsParams) {
|
|
462
469
|
if (props && param.name in props) continue
|
|
463
470
|
if (param.defaultValue) {
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
entries.push(`${pyStr(param.name)}: ${
|
|
471
|
+
const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
|
|
472
|
+
if (result.ok) {
|
|
473
|
+
entries.push(`${pyStr(param.name)}: ${toPyLiteral(result.value)}`)
|
|
467
474
|
continue
|
|
468
475
|
}
|
|
469
476
|
}
|
|
@@ -539,119 +546,6 @@ function buildPythonProps(
|
|
|
539
546
|
return `{${entries.join(', ')}}`
|
|
540
547
|
}
|
|
541
548
|
|
|
542
|
-
/**
|
|
543
|
-
* Evaluate a signal initializer expression using provided props.
|
|
544
|
-
* Handles: props.initial ?? 0, props.value, literal values.
|
|
545
|
-
*/
|
|
546
|
-
export function evaluateSignalInit(
|
|
547
|
-
expr: string,
|
|
548
|
-
props?: Record<string, unknown>,
|
|
549
|
-
): unknown {
|
|
550
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
551
|
-
if (nullishMatch) {
|
|
552
|
-
const propName = nullishMatch[1]
|
|
553
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
554
|
-
if (props && propName in props) return props[propName]
|
|
555
|
-
return parseLiteral(defaultExpr)
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
559
|
-
if (propsMatch) {
|
|
560
|
-
if (props && propsMatch[1] in props) return props[propsMatch[1]]
|
|
561
|
-
return null
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return parseLiteral(expr)
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function parseLiteral(expr: string): unknown {
|
|
568
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
569
|
-
if (expr === 'true') return true
|
|
570
|
-
if (expr === 'false') return false
|
|
571
|
-
if (expr === '[]') return []
|
|
572
|
-
|
|
573
|
-
{
|
|
574
|
-
const t = expr.trim()
|
|
575
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
576
|
-
const inner = t.slice(1, -1).trim()
|
|
577
|
-
if (!inner) return []
|
|
578
|
-
const out: unknown[] = []
|
|
579
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
580
|
-
if (!seg.trim()) continue
|
|
581
|
-
const parsed = parseLiteral(seg.trim())
|
|
582
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
583
|
-
out.push(parsed)
|
|
584
|
-
}
|
|
585
|
-
return out
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
590
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
591
|
-
|
|
592
|
-
const trimmed = expr.trim()
|
|
593
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
594
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
595
|
-
if (!inner) return {}
|
|
596
|
-
const obj: Record<string, unknown> = {}
|
|
597
|
-
for (const pair of splitTopLevelCommas(inner)) {
|
|
598
|
-
if (!pair.trim()) continue
|
|
599
|
-
const colonIdx = pair.indexOf(':')
|
|
600
|
-
if (colonIdx < 0) return null
|
|
601
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
602
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
603
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
604
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
605
|
-
const parsedVal = parseLiteral(val)
|
|
606
|
-
if (parsedVal === null && val !== 'null') return null
|
|
607
|
-
obj[key] = parsedVal
|
|
608
|
-
}
|
|
609
|
-
return obj
|
|
610
|
-
}
|
|
611
|
-
return null
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
615
|
-
const segments: string[] = []
|
|
616
|
-
let depth = 0
|
|
617
|
-
let start = 0
|
|
618
|
-
let quote: string | null = null
|
|
619
|
-
for (let i = 0; i < inner.length; i++) {
|
|
620
|
-
const c = inner[i]
|
|
621
|
-
if (quote) {
|
|
622
|
-
if (c === quote) {
|
|
623
|
-
let backslashes = 0
|
|
624
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
625
|
-
if (backslashes % 2 === 0) quote = null
|
|
626
|
-
}
|
|
627
|
-
continue
|
|
628
|
-
}
|
|
629
|
-
if (c === '"' || c === "'") {
|
|
630
|
-
quote = c
|
|
631
|
-
continue
|
|
632
|
-
}
|
|
633
|
-
if (c === '{' || c === '[') depth++
|
|
634
|
-
else if (c === '}' || c === ']') depth--
|
|
635
|
-
else if (c === ',' && depth === 0) {
|
|
636
|
-
segments.push(inner.slice(start, i))
|
|
637
|
-
start = i + 1
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
segments.push(inner.slice(start))
|
|
641
|
-
return segments
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function unescapeJsString(s: string): string {
|
|
645
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
646
|
-
switch (c) {
|
|
647
|
-
case 'n': return '\n'
|
|
648
|
-
case 'r': return '\r'
|
|
649
|
-
case 't': return '\t'
|
|
650
|
-
case '0': return '\0'
|
|
651
|
-
default: return c
|
|
652
|
-
}
|
|
653
|
-
})
|
|
654
|
-
}
|
|
655
549
|
|
|
656
550
|
/**
|
|
657
551
|
* Python string literal for arbitrary text, via `JSON.stringify`. JSON's
|
|
@@ -689,26 +583,3 @@ function toPyLiteral(value: unknown): string {
|
|
|
689
583
|
return 'None'
|
|
690
584
|
}
|
|
691
585
|
|
|
692
|
-
/**
|
|
693
|
-
* Convert a JS literal value to a Python literal.
|
|
694
|
-
* Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default.
|
|
695
|
-
*/
|
|
696
|
-
function jsToPyValue(jsValue: string): string | null {
|
|
697
|
-
const v = jsValue.trim()
|
|
698
|
-
|
|
699
|
-
if (/^-?\d+(\.\d+)?$/.test(v)) return v
|
|
700
|
-
// A JS string literal (single- or double-quoted) is, character-for-character,
|
|
701
|
-
// ALSO a valid Python string literal for the common escape sequences both
|
|
702
|
-
// languages share — pass it through verbatim rather than re-quoting.
|
|
703
|
-
if (/^['"].*['"]$/.test(v)) return v
|
|
704
|
-
if (v === 'true') return 'True'
|
|
705
|
-
if (v === 'false') return 'False'
|
|
706
|
-
if (v === '[]') return '[]'
|
|
707
|
-
|
|
708
|
-
const nullishMatch = v.match(/\?\?\s*(.+)$/)
|
|
709
|
-
if (nullishMatch) return jsToPyValue(nullishMatch[1])
|
|
710
|
-
|
|
711
|
-
if (v.startsWith('props.')) return 'None'
|
|
712
|
-
|
|
713
|
-
return null
|
|
714
|
-
}
|