@barefootjs/rust 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/lib/static-value.d.ts +13 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/minijinja-adapter.d.ts +41 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -1
- 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__/minijinja-adapter-unit.test.ts +119 -0
- package/src/adapter/lib/static-value.ts +39 -0
- package/src/adapter/minijinja-adapter.ts +102 -5
- package/src/conformance-pins.ts +24 -25
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +11 -144
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
|
|
@@ -187686,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187686
187692
|
return `bf.flat(${recv}, ${d})`;
|
|
187687
187693
|
}
|
|
187688
187694
|
|
|
187695
|
+
// src/adapter/lib/static-value.ts
|
|
187696
|
+
function staticValueToMinijinja(value) {
|
|
187697
|
+
if (value === null || value === undefined)
|
|
187698
|
+
return "none";
|
|
187699
|
+
if (typeof value === "boolean")
|
|
187700
|
+
return value ? "true" : "false";
|
|
187701
|
+
if (typeof value === "number")
|
|
187702
|
+
return String(value);
|
|
187703
|
+
if (typeof value === "string")
|
|
187704
|
+
return `'${escapeMinijinjaSingleQuoted(value)}'`;
|
|
187705
|
+
if (Array.isArray(value)) {
|
|
187706
|
+
const items = [];
|
|
187707
|
+
for (const el of value) {
|
|
187708
|
+
const serialized = staticValueToMinijinja(el);
|
|
187709
|
+
if (serialized === null)
|
|
187710
|
+
return null;
|
|
187711
|
+
items.push(serialized);
|
|
187712
|
+
}
|
|
187713
|
+
return `[${items.join(", ")}]`;
|
|
187714
|
+
}
|
|
187715
|
+
if (typeof value === "object") {
|
|
187716
|
+
const entries = [];
|
|
187717
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187718
|
+
const serialized = staticValueToMinijinja(val);
|
|
187719
|
+
if (serialized === null)
|
|
187720
|
+
return null;
|
|
187721
|
+
entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
|
|
187722
|
+
}
|
|
187723
|
+
return `{${entries.join(", ")}}`;
|
|
187724
|
+
}
|
|
187725
|
+
return null;
|
|
187726
|
+
}
|
|
187727
|
+
|
|
187689
187728
|
// src/adapter/expr/emitters.ts
|
|
187690
187729
|
import {
|
|
187691
187730
|
groupBinaryOperand,
|
|
@@ -188276,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188276
188315
|
_searchParamsLocals = new Set;
|
|
188277
188316
|
_loweringMatchers = [];
|
|
188278
188317
|
localConstants = [];
|
|
188318
|
+
staticLoopSourceBoundNames = new Set;
|
|
188279
188319
|
nullableOptionalProps = new Set;
|
|
188280
188320
|
constructor(options = {}) {
|
|
188281
188321
|
super();
|
|
@@ -188291,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188291
188331
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188292
188332
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188293
188333
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188334
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188294
188335
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188295
188336
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188296
188337
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188412,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188412
188453
|
renderElement(element) {
|
|
188413
188454
|
const tag = element.tag;
|
|
188414
188455
|
const attrs = this.renderAttributes(element);
|
|
188415
|
-
const
|
|
188456
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188457
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188416
188458
|
let hydrationAttrs = "";
|
|
188417
188459
|
if (element.needsScope) {
|
|
188418
188460
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188447,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188447
188489
|
}
|
|
188448
188490
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188449
188491
|
}
|
|
188492
|
+
renderDangerousInnerHtml(element) {
|
|
188493
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188494
|
+
if (!resolution)
|
|
188495
|
+
return null;
|
|
188496
|
+
if (resolution.kind === "dynamic") {
|
|
188497
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188498
|
+
return "";
|
|
188499
|
+
}
|
|
188500
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188501
|
+
if (violation) {
|
|
188502
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188503
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188504
|
+
return "";
|
|
188505
|
+
}
|
|
188506
|
+
return resolution.html;
|
|
188507
|
+
}
|
|
188450
188508
|
renderExpression(expr) {
|
|
188451
188509
|
if (expr.clientOnly) {
|
|
188452
188510
|
if (expr.slotId) {
|
|
@@ -188454,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188454
188512
|
}
|
|
188455
188513
|
return "";
|
|
188456
188514
|
}
|
|
188457
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188515
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188458
188516
|
if (expr.slotId) {
|
|
188459
188517
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188460
188518
|
}
|
|
@@ -188545,8 +188603,12 @@ ${whenTrue}
|
|
|
188545
188603
|
}
|
|
188546
188604
|
});
|
|
188547
188605
|
}
|
|
188606
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188607
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188608
|
+
});
|
|
188609
|
+
const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
|
|
188548
188610
|
const arrayName = loop.array.trim();
|
|
188549
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188611
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188550
188612
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188551
188613
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188552
188614
|
this.errors.push({
|
|
@@ -188560,7 +188622,7 @@ ${whenTrue}
|
|
|
188560
188622
|
});
|
|
188561
188623
|
}
|
|
188562
188624
|
}
|
|
188563
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188625
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188564
188626
|
let array = rawArray;
|
|
188565
188627
|
if (loop.sortComparator) {
|
|
188566
188628
|
const sort = loop.sortComparator;
|
|
@@ -188872,6 +188934,8 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188872
188934
|
for (const attr of element.attrs) {
|
|
188873
188935
|
if (attr.clientOnly)
|
|
188874
188936
|
continue;
|
|
188937
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188938
|
+
continue;
|
|
188875
188939
|
let attrName;
|
|
188876
188940
|
if (attr.name === "className")
|
|
188877
188941
|
attrName = "class";
|
|
@@ -189071,6 +189135,8 @@ Options:
|
|
|
189071
189135
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189072
189136
|
}
|
|
189073
189137
|
_resolveLiteralConst(name) {
|
|
189138
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189139
|
+
return null;
|
|
189074
189140
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189075
189141
|
if (c?.value === undefined)
|
|
189076
189142
|
return null;
|
|
@@ -189083,6 +189149,8 @@ Options:
|
|
|
189083
189149
|
return null;
|
|
189084
189150
|
}
|
|
189085
189151
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189152
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189153
|
+
return null;
|
|
189086
189154
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189087
189155
|
if (!hit)
|
|
189088
189156
|
return null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* MiniJinja 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
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function staticValueToMinijinja(value: unknown): string | null;
|
|
13
|
+
//# 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;;;;;;;;;;GAUG;AAIH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwBpE"}
|
|
@@ -193,6 +193,16 @@ export declare class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitt
|
|
|
193
193
|
* const (`const sizeAttrs = size ? {…} : {}`) to its initializer text.
|
|
194
194
|
*/
|
|
195
195
|
private localConstants;
|
|
196
|
+
/**
|
|
197
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
198
|
+
* parameter anywhere in the component (#2208 fable review). A static
|
|
199
|
+
* loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
|
|
200
|
+
* never resolve through `resolveStaticLoopSource` at a use site where a
|
|
201
|
+
* DIFFERENT, enclosing loop's own callback param shadows it — same
|
|
202
|
+
* shadowing hazard, and same coarse-but-safe mitigation, as #2212's
|
|
203
|
+
* `collectLoopBoundNames` use in `collectStringValueNames`.
|
|
204
|
+
*/
|
|
205
|
+
private staticLoopSourceBoundNames;
|
|
196
206
|
/**
|
|
197
207
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
198
208
|
* Their bare-reference attribute emission is guarded with a Jinja
|
|
@@ -244,6 +254,13 @@ export declare class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitt
|
|
|
244
254
|
private providerObjectLiteralJinja;
|
|
245
255
|
emitAsync(node: IRAsync, _ctx: JinjaRenderCtx, _emit: EmitIRNode<JinjaRenderCtx>): string;
|
|
246
256
|
renderElement(element: IRElement): string;
|
|
257
|
+
/**
|
|
258
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
259
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
260
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
261
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
262
|
+
*/
|
|
263
|
+
private renderDangerousInnerHtml;
|
|
247
264
|
renderExpression(expr: IRExpression): string;
|
|
248
265
|
renderConditional(cond: IRConditional): string;
|
|
249
266
|
private renderNodeOrNull;
|
|
@@ -417,8 +434,32 @@ export declare class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitt
|
|
|
417
434
|
* single-quoted string literal (`const totalPages = 5`, #1897
|
|
418
435
|
* pagination) — function-scope consts never reach the per-render
|
|
419
436
|
* context, so a bare reference would resolve to Undefined.
|
|
437
|
+
*
|
|
438
|
+
* The lookup is a flat name match with no notion of AST scope, so a
|
|
439
|
+
* name that any loop callback binds as its item/index param never
|
|
440
|
+
* inlines (#2221) — the occurrence may be the loop's own (shadowing)
|
|
441
|
+
* binding, and substituting the outer const's value there renders every
|
|
442
|
+
* iteration with the same hard-coded literal. Coarse (a genuinely
|
|
443
|
+
* non-shadowed same-named const elsewhere in the component also stops
|
|
444
|
+
* inlining, falling back to the bare identifier) but safe — the same
|
|
445
|
+
* trade-off as #2212's `collectLoopBoundNames` use in
|
|
446
|
+
* `collectStringValueNames`.
|
|
420
447
|
*/
|
|
421
448
|
private _resolveLiteralConst;
|
|
449
|
+
/**
|
|
450
|
+
* Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
|
|
451
|
+
* (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
|
|
452
|
+
*
|
|
453
|
+
* The lookup is a flat name match on `objectName` with no notion of AST
|
|
454
|
+
* scope, so an enclosing loop callback's own param of the same name
|
|
455
|
+
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
456
|
+
* still resolved to the OUTER const's member value at every iteration
|
|
457
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
|
|
458
|
+
* coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
|
|
459
|
+
* binds anywhere in the component never inlines, falling back to the bare
|
|
460
|
+
* `cfg.x` member expression (which a minijinja `for` loop binds correctly
|
|
461
|
+
* at the shadowed occurrences).
|
|
462
|
+
*/
|
|
422
463
|
private _resolveStaticRecordLiteral;
|
|
423
464
|
private _resolveModuleStringConst;
|
|
424
465
|
private _recordExprBF101;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"minijinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/minijinja-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiHG;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":"minijinja-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/minijinja-adapter.ts"],"names":[],"mappings":"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"}
|
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
|
|
@@ -187686,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187686
187692
|
return `bf.flat(${recv}, ${d})`;
|
|
187687
187693
|
}
|
|
187688
187694
|
|
|
187695
|
+
// src/adapter/lib/static-value.ts
|
|
187696
|
+
function staticValueToMinijinja(value) {
|
|
187697
|
+
if (value === null || value === undefined)
|
|
187698
|
+
return "none";
|
|
187699
|
+
if (typeof value === "boolean")
|
|
187700
|
+
return value ? "true" : "false";
|
|
187701
|
+
if (typeof value === "number")
|
|
187702
|
+
return String(value);
|
|
187703
|
+
if (typeof value === "string")
|
|
187704
|
+
return `'${escapeMinijinjaSingleQuoted(value)}'`;
|
|
187705
|
+
if (Array.isArray(value)) {
|
|
187706
|
+
const items = [];
|
|
187707
|
+
for (const el of value) {
|
|
187708
|
+
const serialized = staticValueToMinijinja(el);
|
|
187709
|
+
if (serialized === null)
|
|
187710
|
+
return null;
|
|
187711
|
+
items.push(serialized);
|
|
187712
|
+
}
|
|
187713
|
+
return `[${items.join(", ")}]`;
|
|
187714
|
+
}
|
|
187715
|
+
if (typeof value === "object") {
|
|
187716
|
+
const entries = [];
|
|
187717
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187718
|
+
const serialized = staticValueToMinijinja(val);
|
|
187719
|
+
if (serialized === null)
|
|
187720
|
+
return null;
|
|
187721
|
+
entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
|
|
187722
|
+
}
|
|
187723
|
+
return `{${entries.join(", ")}}`;
|
|
187724
|
+
}
|
|
187725
|
+
return null;
|
|
187726
|
+
}
|
|
187727
|
+
|
|
187689
187728
|
// src/adapter/expr/emitters.ts
|
|
187690
187729
|
import {
|
|
187691
187730
|
groupBinaryOperand,
|
|
@@ -188276,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188276
188315
|
_searchParamsLocals = new Set;
|
|
188277
188316
|
_loweringMatchers = [];
|
|
188278
188317
|
localConstants = [];
|
|
188318
|
+
staticLoopSourceBoundNames = new Set;
|
|
188279
188319
|
nullableOptionalProps = new Set;
|
|
188280
188320
|
constructor(options = {}) {
|
|
188281
188321
|
super();
|
|
@@ -188291,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188291
188331
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188292
188332
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188293
188333
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188334
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188294
188335
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188295
188336
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188296
188337
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188412,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188412
188453
|
renderElement(element) {
|
|
188413
188454
|
const tag = element.tag;
|
|
188414
188455
|
const attrs = this.renderAttributes(element);
|
|
188415
|
-
const
|
|
188456
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188457
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188416
188458
|
let hydrationAttrs = "";
|
|
188417
188459
|
if (element.needsScope) {
|
|
188418
188460
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188447,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188447
188489
|
}
|
|
188448
188490
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188449
188491
|
}
|
|
188492
|
+
renderDangerousInnerHtml(element) {
|
|
188493
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188494
|
+
if (!resolution)
|
|
188495
|
+
return null;
|
|
188496
|
+
if (resolution.kind === "dynamic") {
|
|
188497
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188498
|
+
return "";
|
|
188499
|
+
}
|
|
188500
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188501
|
+
if (violation) {
|
|
188502
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188503
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188504
|
+
return "";
|
|
188505
|
+
}
|
|
188506
|
+
return resolution.html;
|
|
188507
|
+
}
|
|
188450
188508
|
renderExpression(expr) {
|
|
188451
188509
|
if (expr.clientOnly) {
|
|
188452
188510
|
if (expr.slotId) {
|
|
@@ -188454,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188454
188512
|
}
|
|
188455
188513
|
return "";
|
|
188456
188514
|
}
|
|
188457
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188515
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188458
188516
|
if (expr.slotId) {
|
|
188459
188517
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188460
188518
|
}
|
|
@@ -188545,8 +188603,12 @@ ${whenTrue}
|
|
|
188545
188603
|
}
|
|
188546
188604
|
});
|
|
188547
188605
|
}
|
|
188606
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188607
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188608
|
+
});
|
|
188609
|
+
const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
|
|
188548
188610
|
const arrayName = loop.array.trim();
|
|
188549
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188611
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188550
188612
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188551
188613
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188552
188614
|
this.errors.push({
|
|
@@ -188560,7 +188622,7 @@ ${whenTrue}
|
|
|
188560
188622
|
});
|
|
188561
188623
|
}
|
|
188562
188624
|
}
|
|
188563
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188625
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188564
188626
|
let array = rawArray;
|
|
188565
188627
|
if (loop.sortComparator) {
|
|
188566
188628
|
const sort = loop.sortComparator;
|
|
@@ -188872,6 +188934,8 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188872
188934
|
for (const attr of element.attrs) {
|
|
188873
188935
|
if (attr.clientOnly)
|
|
188874
188936
|
continue;
|
|
188937
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188938
|
+
continue;
|
|
188875
188939
|
let attrName;
|
|
188876
188940
|
if (attr.name === "className")
|
|
188877
188941
|
attrName = "class";
|
|
@@ -189071,6 +189135,8 @@ Options:
|
|
|
189071
189135
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189072
189136
|
}
|
|
189073
189137
|
_resolveLiteralConst(name) {
|
|
189138
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189139
|
+
return null;
|
|
189074
189140
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189075
189141
|
if (c?.value === undefined)
|
|
189076
189142
|
return null;
|
|
@@ -189083,6 +189149,8 @@ Options:
|
|
|
189083
189149
|
return null;
|
|
189084
189150
|
}
|
|
189085
189151
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189152
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189153
|
+
return null;
|
|
189086
189154
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189087
189155
|
if (!hit)
|
|
189088
189156
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;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;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eA4F7B,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
|
|
@@ -187686,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187686
187692
|
return `bf.flat(${recv}, ${d})`;
|
|
187687
187693
|
}
|
|
187688
187694
|
|
|
187695
|
+
// src/adapter/lib/static-value.ts
|
|
187696
|
+
function staticValueToMinijinja(value) {
|
|
187697
|
+
if (value === null || value === undefined)
|
|
187698
|
+
return "none";
|
|
187699
|
+
if (typeof value === "boolean")
|
|
187700
|
+
return value ? "true" : "false";
|
|
187701
|
+
if (typeof value === "number")
|
|
187702
|
+
return String(value);
|
|
187703
|
+
if (typeof value === "string")
|
|
187704
|
+
return `'${escapeMinijinjaSingleQuoted(value)}'`;
|
|
187705
|
+
if (Array.isArray(value)) {
|
|
187706
|
+
const items = [];
|
|
187707
|
+
for (const el of value) {
|
|
187708
|
+
const serialized = staticValueToMinijinja(el);
|
|
187709
|
+
if (serialized === null)
|
|
187710
|
+
return null;
|
|
187711
|
+
items.push(serialized);
|
|
187712
|
+
}
|
|
187713
|
+
return `[${items.join(", ")}]`;
|
|
187714
|
+
}
|
|
187715
|
+
if (typeof value === "object") {
|
|
187716
|
+
const entries = [];
|
|
187717
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187718
|
+
const serialized = staticValueToMinijinja(val);
|
|
187719
|
+
if (serialized === null)
|
|
187720
|
+
return null;
|
|
187721
|
+
entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
|
|
187722
|
+
}
|
|
187723
|
+
return `{${entries.join(", ")}}`;
|
|
187724
|
+
}
|
|
187725
|
+
return null;
|
|
187726
|
+
}
|
|
187727
|
+
|
|
187689
187728
|
// src/adapter/expr/emitters.ts
|
|
187690
187729
|
import {
|
|
187691
187730
|
groupBinaryOperand,
|
|
@@ -188276,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188276
188315
|
_searchParamsLocals = new Set;
|
|
188277
188316
|
_loweringMatchers = [];
|
|
188278
188317
|
localConstants = [];
|
|
188318
|
+
staticLoopSourceBoundNames = new Set;
|
|
188279
188319
|
nullableOptionalProps = new Set;
|
|
188280
188320
|
constructor(options = {}) {
|
|
188281
188321
|
super();
|
|
@@ -188291,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188291
188331
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188292
188332
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188293
188333
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188334
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188294
188335
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188295
188336
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188296
188337
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188412,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188412
188453
|
renderElement(element) {
|
|
188413
188454
|
const tag = element.tag;
|
|
188414
188455
|
const attrs = this.renderAttributes(element);
|
|
188415
|
-
const
|
|
188456
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188457
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188416
188458
|
let hydrationAttrs = "";
|
|
188417
188459
|
if (element.needsScope) {
|
|
188418
188460
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188447,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188447
188489
|
}
|
|
188448
188490
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188449
188491
|
}
|
|
188492
|
+
renderDangerousInnerHtml(element) {
|
|
188493
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188494
|
+
if (!resolution)
|
|
188495
|
+
return null;
|
|
188496
|
+
if (resolution.kind === "dynamic") {
|
|
188497
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188498
|
+
return "";
|
|
188499
|
+
}
|
|
188500
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188501
|
+
if (violation) {
|
|
188502
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188503
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188504
|
+
return "";
|
|
188505
|
+
}
|
|
188506
|
+
return resolution.html;
|
|
188507
|
+
}
|
|
188450
188508
|
renderExpression(expr) {
|
|
188451
188509
|
if (expr.clientOnly) {
|
|
188452
188510
|
if (expr.slotId) {
|
|
@@ -188454,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188454
188512
|
}
|
|
188455
188513
|
return "";
|
|
188456
188514
|
}
|
|
188457
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188515
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188458
188516
|
if (expr.slotId) {
|
|
188459
188517
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188460
188518
|
}
|
|
@@ -188545,8 +188603,12 @@ ${whenTrue}
|
|
|
188545
188603
|
}
|
|
188546
188604
|
});
|
|
188547
188605
|
}
|
|
188606
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188607
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188608
|
+
});
|
|
188609
|
+
const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
|
|
188548
188610
|
const arrayName = loop.array.trim();
|
|
188549
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188611
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188550
188612
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188551
188613
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188552
188614
|
this.errors.push({
|
|
@@ -188560,7 +188622,7 @@ ${whenTrue}
|
|
|
188560
188622
|
});
|
|
188561
188623
|
}
|
|
188562
188624
|
}
|
|
188563
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188625
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188564
188626
|
let array = rawArray;
|
|
188565
188627
|
if (loop.sortComparator) {
|
|
188566
188628
|
const sort = loop.sortComparator;
|
|
@@ -188872,6 +188934,8 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188872
188934
|
for (const attr of element.attrs) {
|
|
188873
188935
|
if (attr.clientOnly)
|
|
188874
188936
|
continue;
|
|
188937
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188938
|
+
continue;
|
|
188875
188939
|
let attrName;
|
|
188876
188940
|
if (attr.name === "className")
|
|
188877
188941
|
attrName = "class";
|
|
@@ -189071,6 +189135,8 @@ Options:
|
|
|
189071
189135
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189072
189136
|
}
|
|
189073
189137
|
_resolveLiteralConst(name) {
|
|
189138
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189139
|
+
return null;
|
|
189074
189140
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189075
189141
|
if (c?.value === undefined)
|
|
189076
189142
|
return null;
|
|
@@ -189083,6 +189149,8 @@ Options:
|
|
|
189083
189149
|
return null;
|
|
189084
189150
|
}
|
|
189085
189151
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189152
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189153
|
+
return null;
|
|
189086
189154
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189087
189155
|
if (!hit)
|
|
189088
189156
|
return null;
|
|
@@ -189120,14 +189188,10 @@ Options:
|
|
|
189120
189188
|
var minijinjaAdapter = new MinijinjaAdapter;
|
|
189121
189189
|
// src/conformance-pins.ts
|
|
189122
189190
|
var conformancePins = {
|
|
189123
|
-
"static-array-children": [{ code: "BF103", severity: "error" }],
|
|
189124
|
-
"todo-app": [{ code: "BF103", severity: "error" }],
|
|
189125
|
-
"todo-app-ssr": [{ code: "BF103", severity: "error" }],
|
|
189126
189191
|
"static-array-from-props": [
|
|
189127
189192
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189128
189193
|
],
|
|
189129
189194
|
"static-array-from-props-with-component": [
|
|
189130
|
-
{ code: "BF103", severity: "error" },
|
|
189131
189195
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189132
189196
|
],
|
|
189133
189197
|
"filter-nested-callback-predicate": [
|
|
@@ -189136,8 +189200,7 @@ var conformancePins = {
|
|
|
189136
189200
|
"filter-nested-find-predicate": [
|
|
189137
189201
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189138
189202
|
],
|
|
189139
|
-
"
|
|
189140
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189203
|
+
"dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
|
|
189141
189204
|
};
|
|
189142
189205
|
// src/render-divergences.ts
|
|
189143
189206
|
var renderDivergences = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;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;;;;;;;;;;;;;;GAcG;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/rust",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
4
4
|
"description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
"directory": "packages/adapter-rust"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.18.
|
|
57
|
+
"@barefootjs/shared": "0.18.7"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@barefootjs/jsx": ">=0.2.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
64
|
-
"@barefootjs/jsx": "0.18.
|
|
64
|
+
"@barefootjs/jsx": "0.18.7",
|
|
65
65
|
"typescript": "^5.0.0"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -411,3 +411,122 @@ export function Parent() {
|
|
|
411
411
|
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and
|
|
412
412
|
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
413
413
|
// twin, which must render clean).
|
|
414
|
+
|
|
415
|
+
// #2221: `_resolveLiteralConst` is a flat name lookup against
|
|
416
|
+
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
417
|
+
// substitute an outer const's literal value even at an occurrence that is
|
|
418
|
+
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
419
|
+
// iteration rendered the same hard-coded literal. Guarded with the same
|
|
420
|
+
// coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
|
|
421
|
+
// anywhere in the component never inlines, falling back to the bare
|
|
422
|
+
// identifier.
|
|
423
|
+
describe('MinijinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
424
|
+
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
425
|
+
const { template } = compileAndGenerate(`
|
|
426
|
+
function Widget() {
|
|
427
|
+
const label: string = 'x'
|
|
428
|
+
return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
429
|
+
}
|
|
430
|
+
`)
|
|
431
|
+
// The loop body must reference the per-iteration loop var...
|
|
432
|
+
expect(template).toContain('1 + label')
|
|
433
|
+
// ...never the outer const's hard-coded value.
|
|
434
|
+
expect(template).not.toContain("1 + 'x'")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
test('a numeric const shadowed by a loop param emits the identifier too', () => {
|
|
438
|
+
const { template } = compileAndGenerate(`
|
|
439
|
+
function Widget() {
|
|
440
|
+
const count = 7
|
|
441
|
+
return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
|
|
442
|
+
}
|
|
443
|
+
`)
|
|
444
|
+
expect(template).toContain('1 + count')
|
|
445
|
+
expect(template).not.toContain('1 + 7')
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
|
|
449
|
+
const { template } = compileAndGenerate(`
|
|
450
|
+
function Widget({ values }: { values: number[] }) {
|
|
451
|
+
const totalPages = 5
|
|
452
|
+
return <div>
|
|
453
|
+
<p>Page 1 of {1 + totalPages}</p>
|
|
454
|
+
<ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
|
|
455
|
+
</div>
|
|
456
|
+
}
|
|
457
|
+
`)
|
|
458
|
+
expect(template).toContain('1 + 5')
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// The accepted coarse-exclusion trade-off (same as #2212): a name that is
|
|
462
|
+
// loop-bound ANYWHERE in the component never inlines, even at a genuinely
|
|
463
|
+
// non-shadowed occurrence outside the loop — the bare identifier is
|
|
464
|
+
// emitted instead of the value.
|
|
465
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
|
|
466
|
+
const { template } = compileAndGenerate(`
|
|
467
|
+
function Widget({ values }: { values: number[] }) {
|
|
468
|
+
const label: string = 'x'
|
|
469
|
+
return <div>
|
|
470
|
+
<p>{1 + label}</p>
|
|
471
|
+
<ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
|
|
472
|
+
</div>
|
|
473
|
+
}
|
|
474
|
+
`)
|
|
475
|
+
expect(template).not.toContain("1 + 'x'")
|
|
476
|
+
expect(template).toContain('2 + label')
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope
|
|
481
|
+
// object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a
|
|
482
|
+
// flat name lookup on `objectName` with no notion of AST scope, the
|
|
483
|
+
// record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to
|
|
484
|
+
// substitute the outer const's member value even at an occurrence that is
|
|
485
|
+
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
486
|
+
// iteration rendered the same hard-coded literal instead of the per-item
|
|
487
|
+
// value. Guarded with the same coarse `staticLoopSourceBoundNames`
|
|
488
|
+
// exclusion as #2221: any name a loop binds anywhere in the component
|
|
489
|
+
// never inlines, falling back to the bare `cfg.x` member expression.
|
|
490
|
+
describe('MinijinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
491
|
+
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
492
|
+
const { template } = compileAndGenerate(`
|
|
493
|
+
const cfg = { x: 'outer-lit' }
|
|
494
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
495
|
+
return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
496
|
+
}
|
|
497
|
+
`)
|
|
498
|
+
// The loop body must reference the per-iteration member access...
|
|
499
|
+
expect(template).toContain('bf.string(cfg.x)')
|
|
500
|
+
// ...never the outer const's hard-coded value.
|
|
501
|
+
expect(template).not.toContain("bf.string('outer-lit')")
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
|
|
505
|
+
const { template } = compileAndGenerate(`
|
|
506
|
+
const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
|
|
507
|
+
function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
508
|
+
return <div>{variantClasses.ghost}</div>
|
|
509
|
+
}
|
|
510
|
+
`)
|
|
511
|
+
expect(template).toContain("bf.string('bg-ghost')")
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
// The accepted coarse-exclusion trade-off (same as #2221/#2212): an
|
|
515
|
+
// object name that is loop-bound ANYWHERE in the component never
|
|
516
|
+
// inlines its member lookups, even at a genuinely non-shadowed
|
|
517
|
+
// occurrence outside the loop — the bare member expression is emitted
|
|
518
|
+
// instead of the value.
|
|
519
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
|
|
520
|
+
const { template } = compileAndGenerate(`
|
|
521
|
+
const cfg = { x: 'outer-lit' }
|
|
522
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
523
|
+
return <div>
|
|
524
|
+
<p>{cfg.x}</p>
|
|
525
|
+
<ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
526
|
+
</div>
|
|
527
|
+
}
|
|
528
|
+
`)
|
|
529
|
+
expect(template).not.toContain("bf.string('outer-lit')")
|
|
530
|
+
expect(template).toContain('bf.string(cfg.x)')
|
|
531
|
+
})
|
|
532
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* MiniJinja 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
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { escapeMinijinjaSingleQuoted, minijinjaHashKey } from './minijinja-naming.ts'
|
|
14
|
+
|
|
15
|
+
export function staticValueToMinijinja(value: unknown): string | null {
|
|
16
|
+
if (value === null || value === undefined) return 'none'
|
|
17
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
18
|
+
if (typeof value === 'number') return String(value)
|
|
19
|
+
if (typeof value === 'string') return `'${escapeMinijinjaSingleQuoted(value)}'`
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
const items: string[] = []
|
|
22
|
+
for (const el of value) {
|
|
23
|
+
const serialized = staticValueToMinijinja(el)
|
|
24
|
+
if (serialized === null) return null
|
|
25
|
+
items.push(serialized)
|
|
26
|
+
}
|
|
27
|
+
return `[${items.join(', ')}]`
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'object') {
|
|
30
|
+
const entries: string[] = []
|
|
31
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
32
|
+
const serialized = staticValueToMinijinja(val)
|
|
33
|
+
if (serialized === null) return null
|
|
34
|
+
entries.push(`${minijinjaHashKey(key)}: ${serialized}`)
|
|
35
|
+
}
|
|
36
|
+
return `{${entries.join(', ')}}`
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
@@ -165,6 +165,12 @@ import {
|
|
|
165
165
|
queryHrefArgs,
|
|
166
166
|
isValidHelperId,
|
|
167
167
|
sortComparatorFromArrow,
|
|
168
|
+
isDangerousInnerHtmlAttr,
|
|
169
|
+
resolveDangerousInnerHtml,
|
|
170
|
+
dangerousInnerHtmlMetacharViolation,
|
|
171
|
+
dangerousInnerHtmlDiagnostic,
|
|
172
|
+
resolveStaticLoopSource,
|
|
173
|
+
collectLoopBoundNames,
|
|
168
174
|
} from '@barefootjs/jsx'
|
|
169
175
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
170
176
|
import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
|
|
@@ -178,6 +184,7 @@ import {
|
|
|
178
184
|
collectRootScopeNodes,
|
|
179
185
|
} from './lib/ir-scope.ts'
|
|
180
186
|
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
187
|
+
import { staticValueToMinijinja } from './lib/static-value.ts'
|
|
181
188
|
import { JinjaFilterEmitter, JinjaTopLevelEmitter, truthyTest } from './expr/emitters.ts'
|
|
182
189
|
import type { JinjaEmitContext, JinjaSpreadContext, JinjaMemoContext } from './emit-context.ts'
|
|
183
190
|
import {
|
|
@@ -314,6 +321,17 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
314
321
|
*/
|
|
315
322
|
private localConstants: IRMetadata['localConstants'] = []
|
|
316
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
326
|
+
* parameter anywhere in the component (#2208 fable review). A static
|
|
327
|
+
* loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
|
|
328
|
+
* never resolve through `resolveStaticLoopSource` at a use site where a
|
|
329
|
+
* DIFFERENT, enclosing loop's own callback param shadows it — same
|
|
330
|
+
* shadowing hazard, and same coarse-but-safe mitigation, as #2212's
|
|
331
|
+
* `collectLoopBoundNames` use in `collectStringValueNames`.
|
|
332
|
+
*/
|
|
333
|
+
private staticLoopSourceBoundNames: Set<string> = new Set()
|
|
334
|
+
|
|
317
335
|
/**
|
|
318
336
|
* Optional, no-default props that are `None` when the caller omits them.
|
|
319
337
|
* Their bare-reference attribute emission is guarded with a Jinja
|
|
@@ -346,6 +364,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
346
364
|
// ("True"/"False") (#1897, pagination's data-active).
|
|
347
365
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
348
366
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
367
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
|
|
349
368
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
350
369
|
this.stringValueNames = collectStringValueNames(ir)
|
|
351
370
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -568,7 +587,8 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
568
587
|
renderElement(element: IRElement): string {
|
|
569
588
|
const tag = element.tag
|
|
570
589
|
const attrs = this.renderAttributes(element)
|
|
571
|
-
const
|
|
590
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
591
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
572
592
|
|
|
573
593
|
let hydrationAttrs = ''
|
|
574
594
|
if (element.needsScope) {
|
|
@@ -603,6 +623,28 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
603
623
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
604
624
|
}
|
|
605
625
|
|
|
626
|
+
/**
|
|
627
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
628
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
629
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
630
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
631
|
+
*/
|
|
632
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
633
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
634
|
+
if (!resolution) return null
|
|
635
|
+
if (resolution.kind === 'dynamic') {
|
|
636
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
637
|
+
return ''
|
|
638
|
+
}
|
|
639
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
640
|
+
if (violation) {
|
|
641
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
642
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
643
|
+
return ''
|
|
644
|
+
}
|
|
645
|
+
return resolution.html
|
|
646
|
+
}
|
|
647
|
+
|
|
606
648
|
// ===========================================================================
|
|
607
649
|
// Expression Rendering
|
|
608
650
|
// ===========================================================================
|
|
@@ -616,8 +658,12 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
616
658
|
}
|
|
617
659
|
|
|
618
660
|
// Text-position interpolation of a possibly-non-string value — see the
|
|
619
|
-
// file header, divergence 2.
|
|
620
|
-
|
|
661
|
+
// file header, divergence 2. Thread the IR-carried `.parsed` tree
|
|
662
|
+
// through (mirrors go-template's `convertExpressionToGo(expr.expr,
|
|
663
|
+
// classify, expr.parsed)`) so a resolved bare-identifier
|
|
664
|
+
// `.map`/`.filter`/… callback (`resolveCallbackMethodFunctionReferences`,
|
|
665
|
+
// #2206) isn't lost to a fresh, unresolved re-parse of the raw string.
|
|
666
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`
|
|
621
667
|
|
|
622
668
|
if (expr.slotId) {
|
|
623
669
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`
|
|
@@ -762,8 +808,27 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
762
808
|
// corpus only because the widened destructure gate (#2087 Phase A/B)
|
|
763
809
|
// no longer refuses this fixture's `([emoji, users]) => ...` param
|
|
764
810
|
// first. Mirrors adapter-jinja's identical check.
|
|
811
|
+
// #2208: a loop source that is a fully-static array literal — either
|
|
812
|
+
// inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
|
|
813
|
+
// bound to a FUNCTION-scope local const whose initializer has no
|
|
814
|
+
// prop/signal/function-call dependency — inlines as a native MiniJinja
|
|
815
|
+
// list/dict literal below, the same way a module-scope const's value
|
|
816
|
+
// is already seeded. A runtime-computed local (#2069, e.g.
|
|
817
|
+
// `Object.entries(props.tags).filter(...)`) still refuses below.
|
|
818
|
+
// `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
|
|
819
|
+
// param shadowing this identifier (fable review) — never resolve the
|
|
820
|
+
// static const in that case. `rawArray` then falls through to the
|
|
821
|
+
// bare identifier expression below, same as before #2208 — which
|
|
822
|
+
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
823
|
+
// const reference (a loud, conservative refusal, not a silent wrong
|
|
824
|
+
// value).
|
|
825
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
826
|
+
isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
|
|
827
|
+
})
|
|
828
|
+
const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null
|
|
829
|
+
|
|
765
830
|
const arrayName = loop.array.trim()
|
|
766
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
831
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
767
832
|
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
768
833
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
769
834
|
this.errors.push({
|
|
@@ -779,7 +844,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
779
844
|
}
|
|
780
845
|
}
|
|
781
846
|
|
|
782
|
-
const rawArray = this.convertExpressionToJinja(loop.array)
|
|
847
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array)
|
|
783
848
|
// Apply sort if present: wrap the loop array in the shared `bf.sort`
|
|
784
849
|
// helper, binding the sorted result to a per-iteration local so the
|
|
785
850
|
// helper runs once.
|
|
@@ -1434,6 +1499,12 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1434
1499
|
// the unsupported-expression lowering is never reached for a deferred
|
|
1435
1500
|
// predicate (no BF101 / BF102). #1966
|
|
1436
1501
|
if (attr.clientOnly) continue
|
|
1502
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
1503
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
1504
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
1505
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
1506
|
+
// (which would double-report alongside the purpose-built one).
|
|
1507
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
1437
1508
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1438
1509
|
let attrName: string
|
|
1439
1510
|
if (attr.name === 'className') attrName = 'class'
|
|
@@ -1804,8 +1875,19 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1804
1875
|
* single-quoted string literal (`const totalPages = 5`, #1897
|
|
1805
1876
|
* pagination) — function-scope consts never reach the per-render
|
|
1806
1877
|
* context, so a bare reference would resolve to Undefined.
|
|
1878
|
+
*
|
|
1879
|
+
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1880
|
+
* name that any loop callback binds as its item/index param never
|
|
1881
|
+
* inlines (#2221) — the occurrence may be the loop's own (shadowing)
|
|
1882
|
+
* binding, and substituting the outer const's value there renders every
|
|
1883
|
+
* iteration with the same hard-coded literal. Coarse (a genuinely
|
|
1884
|
+
* non-shadowed same-named const elsewhere in the component also stops
|
|
1885
|
+
* inlining, falling back to the bare identifier) but safe — the same
|
|
1886
|
+
* trade-off as #2212's `collectLoopBoundNames` use in
|
|
1887
|
+
* `collectStringValueNames`.
|
|
1807
1888
|
*/
|
|
1808
1889
|
private _resolveLiteralConst(name: string): string | null {
|
|
1890
|
+
if (this.staticLoopSourceBoundNames.has(name)) return null
|
|
1809
1891
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1810
1892
|
if (c?.value === undefined) return null
|
|
1811
1893
|
const v = c.value.trim()
|
|
@@ -1815,7 +1897,22 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1815
1897
|
return null
|
|
1816
1898
|
}
|
|
1817
1899
|
|
|
1900
|
+
/**
|
|
1901
|
+
* Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
|
|
1902
|
+
* (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
|
|
1903
|
+
*
|
|
1904
|
+
* The lookup is a flat name match on `objectName` with no notion of AST
|
|
1905
|
+
* scope, so an enclosing loop callback's own param of the same name
|
|
1906
|
+
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
1907
|
+
* still resolved to the OUTER const's member value at every iteration
|
|
1908
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
|
|
1909
|
+
* coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
|
|
1910
|
+
* binds anywhere in the component never inlines, falling back to the bare
|
|
1911
|
+
* `cfg.x` member expression (which a minijinja `for` loop binds correctly
|
|
1912
|
+
* at the shadowed occurrences).
|
|
1913
|
+
*/
|
|
1818
1914
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1915
|
+
if (this.staticLoopSourceBoundNames.has(objectName)) return null
|
|
1819
1916
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1820
1917
|
if (!hit) return null
|
|
1821
1918
|
return hit.kind === 'number'
|
package/src/conformance-pins.ts
CHANGED
|
@@ -12,15 +12,17 @@
|
|
|
12
12
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
13
13
|
|
|
14
14
|
export const conformancePins: ConformancePins = {
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
16
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
17
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
18
|
+
// BF103 loop-body cross-template check no longer fires spuriously. (Both
|
|
19
|
+
// fixtures are still skipped on this adapter via `render-divergences.ts`
|
|
20
|
+
// — #2209 — for an unrelated signal-seeding gap.)
|
|
21
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
22
|
+
// array-literal initializer is now recognized as fully-static
|
|
23
|
+
// (`resolveStaticLoopSource`) and inlined as a native MiniJinja
|
|
24
|
+
// list/dict literal in the `{% for %}` header, the same way a
|
|
25
|
+
// module-scope const's value is already seeded.
|
|
24
26
|
// The `([emoji, users]) => ...` / `([id, t]) => ...` params in these two
|
|
25
27
|
// fixtures no longer trip BF104 — the destructure itself now lowers
|
|
26
28
|
// cleanly to a native `{% set %}` accessor (#2087 Phase B). But both
|
|
@@ -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 `.map()` callbacks (#2087 Phase B): every shape now
|
|
@@ -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
|
}
|
|
@@ -16,4 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
18
18
|
|
|
19
|
-
export const renderDivergences: RenderDivergences = {
|
|
19
|
+
export const renderDivergences: RenderDivergences = {
|
|
20
|
+
// `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
|
|
21
|
+
// `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
|
|
22
|
+
// instead of a fixed regex-shape catalogue) now correctly seeds `todos`
|
|
23
|
+
// from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
|
|
24
|
+
}
|
package/src/test-render.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* that closes that gap.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
|
|
31
|
+
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
|
|
32
32
|
import type { ComponentIR } from '@barefootjs/jsx'
|
|
33
33
|
import { mkdir, rm } from 'node:fs/promises'
|
|
34
34
|
import { resolve } from 'node:path'
|
|
@@ -170,8 +170,15 @@ export async function renderMinijinjaComponent(options: RenderOptions): Promise<
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// Compile parent source.
|
|
174
|
-
|
|
173
|
+
// Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
|
|
174
|
+
// matches this harness's real behavior — every sibling child template is registered
|
|
175
|
+
// alongside the parent before rendering, so a loop-body cross-template
|
|
176
|
+
// call resolves at render time (#2205).
|
|
177
|
+
const result = compileJSX(source, 'component.tsx', {
|
|
178
|
+
adapter,
|
|
179
|
+
outputIR: true,
|
|
180
|
+
siblingTemplatesRegistered: Boolean(components),
|
|
181
|
+
})
|
|
175
182
|
|
|
176
183
|
const errors = result.errors.filter(e => e.severity === 'error')
|
|
177
184
|
if (errors.length > 0) {
|
|
@@ -441,7 +448,7 @@ function buildVars(
|
|
|
441
448
|
for (const param of ir.metadata.propsParams) {
|
|
442
449
|
if (props && param.name in props) continue
|
|
443
450
|
if (param.defaultValue) {
|
|
444
|
-
const value =
|
|
451
|
+
const value = evaluateSignalInit(param.defaultValue.trim(), props)
|
|
445
452
|
if (value !== null) {
|
|
446
453
|
vars[param.name] = value
|
|
447
454
|
continue
|
|
@@ -510,146 +517,6 @@ function buildVars(
|
|
|
510
517
|
return vars
|
|
511
518
|
}
|
|
512
519
|
|
|
513
|
-
/**
|
|
514
|
-
* Convert a destructure-default's JS source text (`{ size = 'md' }`'s
|
|
515
|
-
* `'md'`) to a real JS value. Near-verbatim port of `buildPythonProps`'s
|
|
516
|
-
* `jsToPyValue` helper — which returned Python SOURCE text (safe to reuse
|
|
517
|
-
* verbatim for a string/numeric literal, since JS and Python share that
|
|
518
|
-
* literal grammar) — ported to resolve directly to the JS runtime value
|
|
519
|
-
* instead, via the shared `parseLiteral` for the literal shapes both
|
|
520
|
-
* versions handle identically. The match ORDER is preserved from
|
|
521
|
-
* `jsToPyValue`: numeric/string/bool/`[]` are checked BEFORE the `??`
|
|
522
|
-
* regex, so a string literal containing a literal `??` substring (e.g.
|
|
523
|
-
* `'a??b'`) is caught by the string-literal branch first, not
|
|
524
|
-
* mis-parsed as a nullish-coalescing default.
|
|
525
|
-
*/
|
|
526
|
-
function jsDefaultToVarValue(jsValue: string): unknown {
|
|
527
|
-
const v = jsValue.trim()
|
|
528
|
-
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v)
|
|
529
|
-
const strMatch = v.match(/^(['"])(.*)\1$/s)
|
|
530
|
-
if (strMatch) return unescapeJsString(strMatch[2])
|
|
531
|
-
if (v === 'true') return true
|
|
532
|
-
if (v === 'false') return false
|
|
533
|
-
if (v === '[]') return []
|
|
534
|
-
const nullishMatch = v.match(/\?\?\s*(.+)$/)
|
|
535
|
-
if (nullishMatch) return jsDefaultToVarValue(nullishMatch[1])
|
|
536
|
-
if (v.startsWith('props.')) return null
|
|
537
|
-
return null
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Evaluate a signal initializer expression using provided props.
|
|
542
|
-
* Handles: props.initial ?? 0, props.value, literal values.
|
|
543
|
-
*/
|
|
544
|
-
export function evaluateSignalInit(
|
|
545
|
-
expr: string,
|
|
546
|
-
props?: Record<string, unknown>,
|
|
547
|
-
): unknown {
|
|
548
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
549
|
-
if (nullishMatch) {
|
|
550
|
-
const propName = nullishMatch[1]
|
|
551
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
552
|
-
if (props && propName in props) return props[propName]
|
|
553
|
-
return parseLiteral(defaultExpr)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
557
|
-
if (propsMatch) {
|
|
558
|
-
if (props && propsMatch[1] in props) return props[propsMatch[1]]
|
|
559
|
-
return null
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return parseLiteral(expr)
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function parseLiteral(expr: string): unknown {
|
|
566
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
567
|
-
if (expr === 'true') return true
|
|
568
|
-
if (expr === 'false') return false
|
|
569
|
-
if (expr === '[]') return []
|
|
570
|
-
|
|
571
|
-
{
|
|
572
|
-
const t = expr.trim()
|
|
573
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
574
|
-
const inner = t.slice(1, -1).trim()
|
|
575
|
-
if (!inner) return []
|
|
576
|
-
const out: unknown[] = []
|
|
577
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
578
|
-
if (!seg.trim()) continue
|
|
579
|
-
const parsed = parseLiteral(seg.trim())
|
|
580
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
581
|
-
out.push(parsed)
|
|
582
|
-
}
|
|
583
|
-
return out
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
588
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
589
|
-
|
|
590
|
-
const trimmed = expr.trim()
|
|
591
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
592
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
593
|
-
if (!inner) return {}
|
|
594
|
-
const obj: Record<string, unknown> = {}
|
|
595
|
-
for (const pair of splitTopLevelCommas(inner)) {
|
|
596
|
-
if (!pair.trim()) continue
|
|
597
|
-
const colonIdx = pair.indexOf(':')
|
|
598
|
-
if (colonIdx < 0) return null
|
|
599
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
600
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
601
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
602
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
603
|
-
const parsedVal = parseLiteral(val)
|
|
604
|
-
if (parsedVal === null && val !== 'null') return null
|
|
605
|
-
obj[key] = parsedVal
|
|
606
|
-
}
|
|
607
|
-
return obj
|
|
608
|
-
}
|
|
609
|
-
return null
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
613
|
-
const segments: string[] = []
|
|
614
|
-
let depth = 0
|
|
615
|
-
let start = 0
|
|
616
|
-
let quote: string | null = null
|
|
617
|
-
for (let i = 0; i < inner.length; i++) {
|
|
618
|
-
const c = inner[i]
|
|
619
|
-
if (quote) {
|
|
620
|
-
if (c === quote) {
|
|
621
|
-
let backslashes = 0
|
|
622
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
623
|
-
if (backslashes % 2 === 0) quote = null
|
|
624
|
-
}
|
|
625
|
-
continue
|
|
626
|
-
}
|
|
627
|
-
if (c === '"' || c === "'") {
|
|
628
|
-
quote = c
|
|
629
|
-
continue
|
|
630
|
-
}
|
|
631
|
-
if (c === '{' || c === '[') depth++
|
|
632
|
-
else if (c === '}' || c === ']') depth--
|
|
633
|
-
else if (c === ',' && depth === 0) {
|
|
634
|
-
segments.push(inner.slice(start, i))
|
|
635
|
-
start = i + 1
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
segments.push(inner.slice(start))
|
|
639
|
-
return segments
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function unescapeJsString(s: string): string {
|
|
643
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
644
|
-
switch (c) {
|
|
645
|
-
case 'n': return '\n'
|
|
646
|
-
case 'r': return '\r'
|
|
647
|
-
case 't': return '\t'
|
|
648
|
-
case '0': return '\0'
|
|
649
|
-
default: return c
|
|
650
|
-
}
|
|
651
|
-
})
|
|
652
|
-
}
|
|
653
520
|
|
|
654
521
|
/**
|
|
655
522
|
* Recursively replace JS's non-finite numbers (`NaN`, `Infinity`,
|