@barefootjs/mojolicious 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 +79 -6
- package/dist/adapter/lib/static-value.d.ts +17 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +15 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +23 -6
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +79 -6
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +81 -15
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts +0 -5
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +165 -4
- package/src/adapter/lib/static-value.ts +47 -0
- package/src/adapter/mojo-adapter.ts +77 -5
- package/src/adapter/props/prop-classes.ts +27 -6
- package/src/conformance-pins.ts +26 -28
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +16 -210
- package/src/__tests__/evaluate-signal-init.test.ts +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
4
4
|
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.18.
|
|
55
|
+
"@barefootjs/shared": "0.18.7"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -60,6 +60,6 @@
|
|
|
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
|
}
|
|
65
65
|
}
|
|
@@ -195,6 +195,166 @@ function Box({ other }: { other?: object }) {
|
|
|
195
195
|
})
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
// #2221: same class of hazard as the Twig-family `_resolveLiteralConst`
|
|
199
|
+
// flat-lookup bug, but this adapter's story is different. `resolveLiteralConst`
|
|
200
|
+
// / `resolveStaticRecordLiteral` (mojo-adapter.ts) already guard against it —
|
|
201
|
+
// they consult `loopBoundNames`, a LIVE ref-counted map that
|
|
202
|
+
// `renderLoop` populates/depopulates as it descends/ascends into each loop
|
|
203
|
+
// body (#1749), not a static whole-component set like the Twig family's
|
|
204
|
+
// `collectLoopBoundNames(ir)`. That makes the guard scope-PRECISE rather
|
|
205
|
+
// than coarse: a name loop-bound only inside one loop still inlines fine
|
|
206
|
+
// at a genuinely separate, non-shadowed occurrence elsewhere in the
|
|
207
|
+
// component (see the third test below) — the Twig-family's documented
|
|
208
|
+
// coarse trade-off (a same-named const anywhere else in the component also
|
|
209
|
+
// stops inlining) does not apply here. So no `staticLoopSourceBoundNames`
|
|
210
|
+
// field was added; the existing live tracking already covers this call
|
|
211
|
+
// site and is strictly more precise.
|
|
212
|
+
//
|
|
213
|
+
// The ONE actual gap found: `emitSpread`'s bare-identifier local-const
|
|
214
|
+
// spread resolution (mojo-adapter.ts, the `this.localConstants.find(...)`
|
|
215
|
+
// call keyed by `trimmed`, `{...attrs}` → `{ … }` hashref, #checkbox/icon)
|
|
216
|
+
// read `this.localConstants` directly with no `loopBoundNames` guard at
|
|
217
|
+
// all — a loop param named the same as an outer conditional-object const
|
|
218
|
+
// (`.map((attrs) => <li {...attrs} />)` shadowing `const attrs = cond ?
|
|
219
|
+
// {…} : {}`) incorrectly forwarded the outer object's literal hashref
|
|
220
|
+
// instead of falling through to the per-iteration `$attrs` value. Fixed
|
|
221
|
+
// with the same `loopBoundNames` guard as the other two call sites.
|
|
222
|
+
//
|
|
223
|
+
// Not covered here (upstream, shared-compiler hazard, out of this
|
|
224
|
+
// package's scope): `key={label}` shadowed by an enclosing loop param of
|
|
225
|
+
// the same name is folded to the OUTER const's literal at IR-generation
|
|
226
|
+
// time (`tryResolveIdentifierAsTemplateLiteral` → `findLocalConst` in
|
|
227
|
+
// `packages/jsx/src/jsx-to-ir.ts`), before any adapter runs — so this
|
|
228
|
+
// adapter (and every other adapter, including Hono's native JSX
|
|
229
|
+
// re-emission) still renders a `key`/`data-key` value shadowed this way
|
|
230
|
+
// as the outer literal, unconditionally, every iteration.
|
|
231
|
+
describe('MojoAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
232
|
+
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
233
|
+
const { template } = compileAndGenerate(`
|
|
234
|
+
function Widget() {
|
|
235
|
+
const label: string = 'x'
|
|
236
|
+
return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
237
|
+
}
|
|
238
|
+
`)
|
|
239
|
+
expect(template).toContain('1 + $label')
|
|
240
|
+
expect(template).not.toContain("1 + 'x'")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('a numeric const shadowed by a loop param emits the identifier too', () => {
|
|
244
|
+
const { template } = compileAndGenerate(`
|
|
245
|
+
function Widget() {
|
|
246
|
+
const count = 7
|
|
247
|
+
return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
|
|
248
|
+
}
|
|
249
|
+
`)
|
|
250
|
+
expect(template).toContain('1 + $count')
|
|
251
|
+
expect(template).not.toContain('1 + 7')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
|
|
255
|
+
const { template } = compileAndGenerate(`
|
|
256
|
+
function Widget({ values }: { values: number[] }) {
|
|
257
|
+
const totalPages = 5
|
|
258
|
+
return <div>
|
|
259
|
+
<p>Page 1 of {1 + totalPages}</p>
|
|
260
|
+
<ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
|
|
261
|
+
</div>
|
|
262
|
+
}
|
|
263
|
+
`)
|
|
264
|
+
expect(template).toContain('1 + 5')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Unlike the Twig-family's coarse-but-safe `collectLoopBoundNames(ir)`
|
|
268
|
+
// exclusion, this adapter's LIVE `loopBoundNames` tracking is scoped to
|
|
269
|
+
// the actual render position: a name loop-bound ONLY inside the `.map`
|
|
270
|
+
// callback still inlines correctly at a separate, non-shadowed
|
|
271
|
+
// occurrence outside the loop — no accepted trade-off here.
|
|
272
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines (more precise than Twig family)', () => {
|
|
273
|
+
const { template } = compileAndGenerate(`
|
|
274
|
+
function Widget({ values }: { values: number[] }) {
|
|
275
|
+
const label: string = 'x'
|
|
276
|
+
return <div>
|
|
277
|
+
<p>{1 + label}</p>
|
|
278
|
+
<ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
|
|
279
|
+
</div>
|
|
280
|
+
}
|
|
281
|
+
`)
|
|
282
|
+
expect(template).toContain("1 + 'x'")
|
|
283
|
+
expect(template).toContain('2 + $label')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// The actual gap this issue found in this adapter: `emitSpread`'s
|
|
287
|
+
// bare-identifier local-const resolution (`{...attrs}` → the outer
|
|
288
|
+
// conditional object's hashref) had no `loopBoundNames` guard.
|
|
289
|
+
test('a loop param shadowing an outer conditional-object const spread emits the loop var, not the outer hashref', () => {
|
|
290
|
+
const { template } = compileAndGenerate(`
|
|
291
|
+
function Widget({ items }: { items: object[] }) {
|
|
292
|
+
const attrs = true ? { 'data-on': 'outer' } : {}
|
|
293
|
+
return <ul>{items.map((attrs) => <li {...attrs} />)}</ul>
|
|
294
|
+
}
|
|
295
|
+
`)
|
|
296
|
+
expect(template).toContain('bf->spread_attrs($attrs)')
|
|
297
|
+
expect(template).not.toContain("'data-on' => 'outer'")
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// #2237: the record-literal sibling of #2221's `resolveLiteralConst` bug —
|
|
302
|
+
// `resolveStaticRecordLiteral` (`IDENT.key` on a module-scope object-literal
|
|
303
|
+
// const, e.g. `variantClasses.ghost` — #1896/#1897) is confirmed reproducible
|
|
304
|
+
// on the Twig-family adapters (flat `objectName` lookup with no notion of AST
|
|
305
|
+
// scope, so an enclosing loop callback's own param of the same name resolved
|
|
306
|
+
// to the OUTER const's member value at every iteration). This adapter's
|
|
307
|
+
// `resolveStaticRecordLiteral` already guards against it (mojo-adapter.ts:
|
|
308
|
+
// `if (this.loopBoundNames?.has?.(objectName)) return null`) — the same LIVE,
|
|
309
|
+
// ref-counted `loopBoundNames` map `resolveLiteralConst` consults (#1749),
|
|
310
|
+
// scope-precise rather than the Twig family's coarse whole-component set.
|
|
311
|
+
// Pinned here (mirroring the #2221 scope-precision pin above) rather than
|
|
312
|
+
// fixed, since no code change was needed.
|
|
313
|
+
describe('MojoAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
314
|
+
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
315
|
+
const { template } = compileAndGenerate(`
|
|
316
|
+
const cfg = { x: 'outer-lit' }
|
|
317
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
318
|
+
return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
319
|
+
}
|
|
320
|
+
`)
|
|
321
|
+
// The loop body must reference the per-iteration member access...
|
|
322
|
+
expect(template).toContain('$cfg->{x}')
|
|
323
|
+
// ...never the outer const's hard-coded value.
|
|
324
|
+
expect(template).not.toContain("'outer-lit'")
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
|
|
328
|
+
const { template } = compileAndGenerate(`
|
|
329
|
+
const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
|
|
330
|
+
function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
331
|
+
return <div>{variantClasses.ghost}</div>
|
|
332
|
+
}
|
|
333
|
+
`)
|
|
334
|
+
expect(template).toContain("'bg-ghost'")
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Unlike the Twig-family's coarse-but-safe `staticLoopSourceBoundNames`
|
|
338
|
+
// exclusion, this adapter's LIVE `loopBoundNames` tracking is scoped to
|
|
339
|
+
// the actual render position: an object name loop-bound ONLY inside the
|
|
340
|
+
// `.map` callback still inlines its member lookup correctly at a
|
|
341
|
+
// separate, non-shadowed occurrence outside the loop — no accepted
|
|
342
|
+
// trade-off here.
|
|
343
|
+
test('an object name loop-bound only inside the loop still inlines its member lookup outside it (more precise than Twig family)', () => {
|
|
344
|
+
const { template } = compileAndGenerate(`
|
|
345
|
+
const cfg = { x: 'outer-lit' }
|
|
346
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
347
|
+
return <div>
|
|
348
|
+
<p>{cfg.x}</p>
|
|
349
|
+
<ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
350
|
+
</div>
|
|
351
|
+
}
|
|
352
|
+
`)
|
|
353
|
+
expect(template).toContain("<p><%= 'outer-lit' %></p>")
|
|
354
|
+
expect(template).toContain('$cfg->{x}')
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
198
358
|
describe('MojoAdapter - Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
|
|
199
359
|
// `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed by
|
|
200
360
|
// a prop inside a conditional-spread object value lowers to an inline
|
|
@@ -1406,7 +1566,7 @@ describe('MojoAdapter - #1448 Tier C .flat(depth?)', () => {
|
|
|
1406
1566
|
function emitFlat(expr: string): string {
|
|
1407
1567
|
const a = new MojoAdapter()
|
|
1408
1568
|
const ir = compileToIR(`
|
|
1409
|
-
function C({ rows }: { rows:
|
|
1569
|
+
function C({ rows }: { rows: { x: string }[][] }) {
|
|
1410
1570
|
return <div>{${expr}}</div>
|
|
1411
1571
|
}
|
|
1412
1572
|
export { C }
|
|
@@ -1526,9 +1686,10 @@ export { C }
|
|
|
1526
1686
|
expect(t).toContain(`"property":"name"`)
|
|
1527
1687
|
})
|
|
1528
1688
|
|
|
1529
|
-
// The function-reference `.map(format)`
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1689
|
+
// The function-reference `.map(format)` case is now covered cross-adapter
|
|
1690
|
+
// by the `array-map-function-reference` shared fixture — `format` resolves
|
|
1691
|
+
// to its declaration (#2206) and the fixture compiles clean rather than
|
|
1692
|
+
// refusing with BF101.
|
|
1532
1693
|
})
|
|
1533
1694
|
|
|
1534
1695
|
describe('MojoAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Perl literal. Used to inline a fully-static loop source (an inline array
|
|
5
|
+
* literal, or a function-scope local const with a static initializer)
|
|
6
|
+
* directly in the loop-bound expression, rather than requiring a bound
|
|
7
|
+
* template variable.
|
|
8
|
+
*
|
|
9
|
+
* Booleans deliberately return `null` (defer to the caller's BF101
|
|
10
|
+
* refusal) rather than baking `1`/`''` — Perl has no boolean literal, and
|
|
11
|
+
* that would diverge from JS's `String(true) === "true"` at render.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` for a value this adapter can't represent as a literal —
|
|
14
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { perlHashKey } from './perl-naming.ts'
|
|
18
|
+
|
|
19
|
+
function escapePerlSingleQuote(s: string): string {
|
|
20
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function staticValueToPerl(value: unknown): string | null {
|
|
24
|
+
if (value === null || value === undefined) return 'undef'
|
|
25
|
+
if (typeof value === 'boolean') return null
|
|
26
|
+
if (typeof value === 'number') return String(value)
|
|
27
|
+
if (typeof value === 'string') return `'${escapePerlSingleQuote(value)}'`
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const items: string[] = []
|
|
30
|
+
for (const el of value) {
|
|
31
|
+
const serialized = staticValueToPerl(el)
|
|
32
|
+
if (serialized === null) return null
|
|
33
|
+
items.push(serialized)
|
|
34
|
+
}
|
|
35
|
+
return `[${items.join(', ')}]`
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === 'object') {
|
|
38
|
+
const entries: string[] = []
|
|
39
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
40
|
+
const serialized = staticValueToPerl(val)
|
|
41
|
+
if (serialized === null) return null
|
|
42
|
+
entries.push(`${perlHashKey(key)} => ${serialized}`)
|
|
43
|
+
}
|
|
44
|
+
return `{ ${entries.join(', ')} }`
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
@@ -59,6 +59,11 @@ import {
|
|
|
59
59
|
isValidHelperId,
|
|
60
60
|
sortComparatorFromArrow,
|
|
61
61
|
isLowerableLoopDestructure,
|
|
62
|
+
isDangerousInnerHtmlAttr,
|
|
63
|
+
resolveDangerousInnerHtml,
|
|
64
|
+
dangerousInnerHtmlMetacharViolation,
|
|
65
|
+
dangerousInnerHtmlDiagnostic,
|
|
66
|
+
resolveStaticLoopSource,
|
|
62
67
|
} from '@barefootjs/jsx'
|
|
63
68
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
64
69
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
@@ -72,6 +77,7 @@ import {
|
|
|
72
77
|
collectRootScopeNodes,
|
|
73
78
|
} from './lib/ir-scope.ts'
|
|
74
79
|
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
80
|
+
import { staticValueToPerl } from './lib/static-value.ts'
|
|
75
81
|
import { MojoFilterEmitter, MojoTopLevelEmitter } from './expr/emitters.ts'
|
|
76
82
|
import type { MojoEmitContext, MojoSpreadContext, MojoMemoContext } from './emit-context.ts'
|
|
77
83
|
import {
|
|
@@ -415,6 +421,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
415
421
|
* quoted string literal (`const totalPages = 5`, #1897 pagination) —
|
|
416
422
|
* function-scope consts never reach the per-render stash, so a bare
|
|
417
423
|
* `$totalPages` faults under strict mode.
|
|
424
|
+
*
|
|
425
|
+
* The `loopBoundNames` guard also covers the #2221 hazard (a loop
|
|
426
|
+
* callback's own param shadowing this outer const's name): unlike the
|
|
427
|
+
* Twig-family adapters' coarse, whole-component `collectLoopBoundNames(ir)`
|
|
428
|
+
* static set, this adapter's `loopBoundNames` is a LIVE ref-counted map
|
|
429
|
+
* `renderLoop` populates/depopulates as it descends/ascends into each
|
|
430
|
+
* loop body (#1749) — so it's already scope-precise for this call site;
|
|
431
|
+
* no separate `staticLoopSourceBoundNames`-style field is needed here.
|
|
418
432
|
*/
|
|
419
433
|
private resolveLiteralConst(name: string): string | null {
|
|
420
434
|
if (this.loopBoundNames?.has?.(name)) return null
|
|
@@ -617,7 +631,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
617
631
|
renderElement(element: IRElement): string {
|
|
618
632
|
const tag = element.tag
|
|
619
633
|
const attrs = this.renderAttributes(element)
|
|
620
|
-
const
|
|
634
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
635
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
621
636
|
|
|
622
637
|
let hydrationAttrs = ''
|
|
623
638
|
if (element.needsScope) {
|
|
@@ -653,6 +668,28 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
653
668
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
654
669
|
}
|
|
655
670
|
|
|
671
|
+
/**
|
|
672
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
673
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
674
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
675
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
676
|
+
*/
|
|
677
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
678
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
679
|
+
if (!resolution) return null
|
|
680
|
+
if (resolution.kind === 'dynamic') {
|
|
681
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
682
|
+
return ''
|
|
683
|
+
}
|
|
684
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
685
|
+
if (violation) {
|
|
686
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
687
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
688
|
+
return ''
|
|
689
|
+
}
|
|
690
|
+
return resolution.html
|
|
691
|
+
}
|
|
692
|
+
|
|
656
693
|
// ===========================================================================
|
|
657
694
|
// Expression Rendering
|
|
658
695
|
// ===========================================================================
|
|
@@ -665,7 +702,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
665
702
|
return ''
|
|
666
703
|
}
|
|
667
704
|
|
|
668
|
-
|
|
705
|
+
// Thread the IR-carried `.parsed` tree through (mirrors go-template's
|
|
706
|
+
// `convertExpressionToGo(expr.expr, classify, expr.parsed)`) so a
|
|
707
|
+
// resolved bare-identifier `.map`/`.filter`/… callback
|
|
708
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206) isn't lost to a
|
|
709
|
+
// fresh, unresolved re-parse of the raw string.
|
|
710
|
+
const perlExpr = this.convertExpressionToPerl(expr.expr, expr.parsed)
|
|
669
711
|
|
|
670
712
|
if (expr.slotId) {
|
|
671
713
|
return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`
|
|
@@ -810,8 +852,24 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
810
852
|
// reachable in this adapter's test corpus only because the widened
|
|
811
853
|
// destructure gate (#2087 Phase A/B) no longer refuses this fixture's
|
|
812
854
|
// `([emoji, users]) => ...` param first.
|
|
855
|
+
// #2208: a loop source that is a fully-static array literal — either
|
|
856
|
+
// inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
|
|
857
|
+
// bound to a FUNCTION-scope local const whose initializer has no
|
|
858
|
+
// prop/signal/function-call dependency — inlines as a native Perl
|
|
859
|
+
// arrayref/hashref literal below, the same way a module-scope const's
|
|
860
|
+
// value is already seeded. A runtime-computed local (#2069, e.g.
|
|
861
|
+
// `Object.entries(props.tags).filter(...)`) still refuses below.
|
|
862
|
+
// `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
|
|
863
|
+
// param shadowing this identifier (fable review) — reuses the same
|
|
864
|
+
// live `loopBoundNames` ref-counted tracking `resolveModuleStringConst`
|
|
865
|
+
// already consults for this hazard class (#1749).
|
|
866
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
867
|
+
isNameShadowed: name => this.loopBoundNames.has(name),
|
|
868
|
+
})
|
|
869
|
+
const staticArray = staticItems !== null ? staticValueToPerl(staticItems) : null
|
|
870
|
+
|
|
813
871
|
const arrayName = loop.array.trim()
|
|
814
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
872
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
815
873
|
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
816
874
|
if (arrayConst && !arrayConst.isModule && this.resolveLiteralConst(arrayName) === null) {
|
|
817
875
|
this.errors.push({
|
|
@@ -827,7 +885,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
827
885
|
}
|
|
828
886
|
}
|
|
829
887
|
|
|
830
|
-
const rawArray = this.convertExpressionToPerl(loop.array)
|
|
888
|
+
const rawArray = staticArray ?? this.convertExpressionToPerl(loop.array)
|
|
831
889
|
// Apply sort if present (#1448 Tier B): wrap the loop array in the
|
|
832
890
|
// shared sort helper. The same `renderSortEval` / `renderSortMethod`
|
|
833
891
|
// pair feeds both this loop-chain hoist and the emitter's
|
|
@@ -1411,7 +1469,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1411
1469
|
// initializer text and route through the same conditional-spread
|
|
1412
1470
|
// lowering. Only function-scope (`!isModule`) consts whose value is
|
|
1413
1471
|
// NOT itself a bare identifier (loop guard) are considered.
|
|
1414
|
-
|
|
1472
|
+
//
|
|
1473
|
+
// `loopBoundNames` guard (#2221): an enclosing `.map()` callback's
|
|
1474
|
+
// own param can shadow this outer const's name (`.map((sizeAttrs)
|
|
1475
|
+
// => <li {...sizeAttrs} />)`) — without the guard this forwarded
|
|
1476
|
+
// the OUTER const's hashref at every iteration instead of the
|
|
1477
|
+
// per-item `$sizeAttrs` value. Same live ref-counted map
|
|
1478
|
+
// `resolveLiteralConst` / `resolveStaticRecordLiteral` already
|
|
1479
|
+
// consult for this hazard class (#1749).
|
|
1480
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
|
|
1415
1481
|
const localConst = this.localConstants.find(
|
|
1416
1482
|
c => c.name === trimmed && !c.isModule,
|
|
1417
1483
|
)
|
|
@@ -1488,6 +1554,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1488
1554
|
// the unsupported-expression lowering is never reached for a deferred
|
|
1489
1555
|
// predicate (no BF101 / BF102). #1966
|
|
1490
1556
|
if (attr.clientOnly) continue
|
|
1557
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
1558
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
1559
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
1560
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
1561
|
+
// (which would double-report alongside the purpose-built one).
|
|
1562
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
1491
1563
|
// Rewrite JSX special-prop names to their HTML-attribute
|
|
1492
1564
|
// counterparts (#1475). `className` → `class` was already
|
|
1493
1565
|
// wired in; the `key` → `data-key` rewrite matches the
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* adapter's `props/prop-types.ts`. No adapter instance state.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { collectLoopBoundNames, type ComponentIR } from '@barefootjs/jsx'
|
|
11
11
|
import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -67,11 +67,28 @@ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* String-typed signals
|
|
71
|
-
* to `eq`/`ne` (#1672)
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* String-typed signals, props, and same-file local consts, so equality
|
|
71
|
+
* comparisons against them lower to `eq`/`ne` (#1672) and `+` concatenation
|
|
72
|
+
* against them lowers to Perl's `.` instead of numeric `+` (#2163, #2212 —
|
|
73
|
+
* `isStringConcatBinary`/`isStringTypedOperand` in `@barefootjs/jsx`, which
|
|
74
|
+
* now also recognizes a bare identifier operand, not just a prop/getter/
|
|
75
|
+
* literal). A signal is string-typed when its inferred type is `string`
|
|
76
|
+
* (the analyzer infers this from a string-literal initial value) or,
|
|
77
|
+
* defensively, when its initial value is a bare string literal; a prop or
|
|
78
|
+
* local const when its annotated (or inferred) type is `string`.
|
|
79
|
+
*
|
|
80
|
+
* Excludes any name bound as a `.map()`/`.filter()` loop callback's item
|
|
81
|
+
* or index parameter ANYWHERE in the component (Fable review, #2212): the
|
|
82
|
+
* lookup below is a flat, scope-blind `Set<string>` with no notion of a
|
|
83
|
+
* loop param shadowing an outer string-typed binding of the same name
|
|
84
|
+
* (`items.map((name) => 1 + name)` inside a component that also has a
|
|
85
|
+
* string `name` prop) — left unguarded, that shadowed `name` would be
|
|
86
|
+
* misdetected as string-typed and `1 + name` would silently lower to `.`
|
|
87
|
+
* instead of staying numeric `+`. Subtracting loop-bound names is coarse
|
|
88
|
+
* (it also suppresses a genuinely non-shadowed same-named string
|
|
89
|
+
* elsewhere in the component) but safe: the suppressed case just falls
|
|
90
|
+
* back to today's numeric `+` — the same, already-accepted residual as an
|
|
91
|
+
* unresolvable operand — never silently-wrong output.
|
|
75
92
|
*/
|
|
76
93
|
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
77
94
|
const names = new Set<string>()
|
|
@@ -83,5 +100,9 @@ export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
|
83
100
|
for (const p of ir.metadata.propsParams) {
|
|
84
101
|
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
85
102
|
}
|
|
103
|
+
for (const c of ir.metadata.localConstants) {
|
|
104
|
+
if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value)) names.add(c.name)
|
|
105
|
+
}
|
|
106
|
+
for (const bound of collectLoopBoundNames(ir)) names.delete(bound)
|
|
86
107
|
return names
|
|
87
108
|
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
10
10
|
|
|
11
11
|
export const conformancePins: ConformancePins = {
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
13
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
14
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
15
|
+
// BF103 loop-body cross-template check no longer fires spuriously. (Both
|
|
16
|
+
// fixtures are still skipped on this adapter via `render-divergences.ts`
|
|
17
|
+
// — #2209 — for an unrelated signal-seeding gap.)
|
|
18
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
19
|
+
// array-literal initializer is now recognized as fully-static
|
|
20
|
+
// (`resolveStaticLoopSource`) and inlined as a native Perl
|
|
21
|
+
// arrayref/hashref literal in the loop-bound expression, the same way a
|
|
22
|
+
// module-scope const's value is already seeded.
|
|
23
23
|
// `([emoji, users]) => ...` / `([id, t]) => ...` are plain array-index
|
|
24
24
|
// (tuple) destructures, no rest — #2087 Phase B's `segments`-walking
|
|
25
25
|
// accessor lowers both to `$__bf_item->[0]` / `$__bf_item->[1]` `my`
|
|
@@ -32,12 +32,11 @@ export const conformancePins: ConformancePins = {
|
|
|
32
32
|
// check in `renderLoop`. This was always true; it was simply
|
|
33
33
|
// unreachable before because BF104 refused the destructure shape first.
|
|
34
34
|
'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
],
|
|
35
|
+
// The BF101 above fires; BF104 no longer does (see above), and BF103
|
|
36
|
+
// (sibling-imported `<Tag>` child component in the loop body) no longer
|
|
37
|
+
// does either now that the conformance harness passes
|
|
38
|
+
// `siblingTemplatesRegistered: true` (#2205).
|
|
39
|
+
'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
|
|
41
40
|
// #1310 / #2087: rest destructure in .map() callback. All four shapes
|
|
42
41
|
// now lower via #2087 Phase B's `segments`-walking accessor:
|
|
43
42
|
// - object-rest read via member access (`rest-destructure-object-in-map`):
|
|
@@ -130,15 +129,14 @@ export const conformancePins: ConformancePins = {
|
|
|
130
129
|
// runtime `bf->find` / `find_index` / `find_last` / `find_last_index` helpers
|
|
131
130
|
// (per-element coderef predicate), matching Xslate. `.join` was never
|
|
132
131
|
// pinned (handled by `renderArrayMethod`'s `case 'join'`).
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
132
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
133
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
134
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
135
|
+
// #2090 established for `.sort(fnref)`.
|
|
136
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
137
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
138
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
139
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
140
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
141
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
144
142
|
}
|
|
@@ -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
|
+
}
|