@barefootjs/jsx 0.33.6 → 0.35.0

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.
Files changed (111) hide show
  1. package/dist/adapters/parsed-expr-emitter.d.ts +17 -0
  2. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/expression-parser.d.ts +17 -4
  5. package/dist/expression-parser.d.ts.map +1 -1
  6. package/dist/index.d.ts +4 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +445 -282
  9. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +11 -0
  14. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts +52 -0
  17. package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
  18. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +2 -0
  19. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +6 -7
  21. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
  23. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -6
  26. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/shared.d.ts +11 -9
  28. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  31. package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
  32. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  33. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  34. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  35. package/dist/ir-to-client-js/html-template.d.ts +2 -2
  36. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  37. package/dist/ir-to-client-js/imports.d.ts +2 -2
  38. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  39. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  40. package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
  41. package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
  42. package/dist/ir-to-client-js/safe-html.d.ts +158 -0
  43. package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
  44. package/dist/ir-to-client-js/utils.d.ts +34 -1
  45. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  46. package/dist/jsx-to-ir.d.ts.map +1 -1
  47. package/dist/lowering-registry.d.ts +12 -0
  48. package/dist/lowering-registry.d.ts.map +1 -1
  49. package/dist/prop-rewrite.d.ts +8 -3
  50. package/dist/prop-rewrite.d.ts.map +1 -1
  51. package/dist/props-binding.d.ts +22 -1
  52. package/dist/props-binding.d.ts.map +1 -1
  53. package/dist/ssr-defaults.d.ts.map +1 -1
  54. package/dist/types.d.ts +5 -10
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +2 -2
  57. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +10 -12
  58. package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
  59. package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
  60. package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
  61. package/src/__tests__/flatmap-segments.test.ts +1 -1
  62. package/src/__tests__/lazy-conditional.test.ts +52 -12
  63. package/src/__tests__/lazy-preamble.test.ts +11 -11
  64. package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
  65. package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
  66. package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
  67. package/src/__tests__/lowering-value-position.test.ts +232 -0
  68. package/src/__tests__/map-arbitrary-body.test.ts +1 -1
  69. package/src/__tests__/nested-loop-index-param.test.ts +14 -3
  70. package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -2
  71. package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
  72. package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
  73. package/src/__tests__/safe-html-splice-door.test.ts +61 -0
  74. package/src/__tests__/ssr-defaults.test.ts +59 -0
  75. package/src/adapters/parsed-expr-emitter.ts +43 -0
  76. package/src/analyzer.ts +4 -0
  77. package/src/expression-parser.ts +63 -29
  78. package/src/index.ts +10 -3
  79. package/src/ir-to-client-js/collect-elements.ts +18 -2
  80. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
  81. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
  82. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +10 -6
  83. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +8 -8
  84. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +18 -2
  85. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +3 -3
  86. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
  87. package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +110 -0
  88. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
  89. package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
  90. package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
  91. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
  92. package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
  93. package/src/ir-to-client-js/control-flow/shared.ts +40 -17
  94. package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
  95. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +10 -0
  96. package/src/ir-to-client-js/csr-substitute.ts +29 -0
  97. package/src/ir-to-client-js/emit-reactive.ts +4 -1
  98. package/src/ir-to-client-js/emit-registration.ts +2 -4
  99. package/src/ir-to-client-js/html-template.ts +58 -244
  100. package/src/ir-to-client-js/imports.ts +3 -0
  101. package/src/ir-to-client-js/index.ts +4 -4
  102. package/src/ir-to-client-js/markup-slots.ts +25 -0
  103. package/src/ir-to-client-js/safe-html.ts +243 -0
  104. package/src/ir-to-client-js/utils.ts +58 -10
  105. package/src/jsx-to-ir.ts +115 -20
  106. package/src/lowering-registry.ts +21 -0
  107. package/src/prop-rewrite.ts +8 -4
  108. package/src/props-binding.ts +37 -1
  109. package/src/ssr-defaults.ts +45 -0
  110. package/src/types.ts +5 -10
  111. package/src/__tests__/text-slot-escaping.test.ts +0 -65
@@ -31,6 +31,8 @@
31
31
 
32
32
  import ts from 'typescript'
33
33
  import { extractFreeIdentifiersFromNode } from './analyzer.ts'
34
+ import { resolveGetterAliases, collectAliasableGetterNames } from './ir-to-client-js/csr-substitute.ts'
35
+ import { resolveBodyDestructuredPropAliases } from './props-binding.ts'
34
36
  import type { IRMetadata } from './types.ts'
35
37
 
36
38
  /**
@@ -311,6 +313,49 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
311
313
  bindings[memo.name] = value
312
314
  }
313
315
 
316
+ // #2813: a bare local-const alias of a signal/memo getter
317
+ // (`const items__alias = items`) is a DIFFERENT identifier from the
318
+ // getter it aliases, but a template-stash adapter's `.map()` loop over
319
+ // `items__alias()` looks up the stash entry BY THAT NAME — which the
320
+ // loops above never create, since they only seed the getter's own
321
+ // name. Reuses `resolveGetterAliases` (the same alias-hop walker
322
+ // #2778's CSR-template fix introduced) rather than a third alias-hop
323
+ // walker: every alias of a getter seeded above gets the SAME entry
324
+ // under its own name, so the stash has both `items` and `items__alias`
325
+ // pointing at one value. Env-signal getters are excluded from the
326
+ // getter-name set (they're never given an `out` entry above — an
327
+ // adapter's own env-signal binding seeds them instead).
328
+ {
329
+ const getterNames = collectAliasableGetterNames(metadata.signals, metadata.memos)
330
+ for (const [alias, origin] of resolveGetterAliases(metadata.localConstants ?? [], (n) => getterNames.has(n))) {
331
+ if (alias in out) continue
332
+ out[alias] = out[origin]
333
+ }
334
+ }
335
+
336
+ // #2788: a bare-props-form component (`function Foo(props: Props)`)
337
+ // whose BODY destructures a prop under a different local name
338
+ // (`const { children: kids } = props`) has no `ParamInfo.sourceName` to
339
+ // key off above — `propsParams` here comes from the TYPE annotation
340
+ // (`extractPropsFromTypeMembers`), which has no notion of a body-level
341
+ // rename. `resolveBodyDestructuredPropAliases` recognizes such a
342
+ // destructuring statement's `localConstants` shape; seeding the LOCAL
343
+ // name too (alongside the caller-facing key already seeded above) is
344
+ // enough to fix what a template-stash adapter's compiled output
345
+ // actually reads (`v[:kids]` / `$kids`). Without this, a stash-based
346
+ // adapter reading the renamed local sees an undefined/nil value instead
347
+ // of the prop — fatal under Perl strict mode (Mojolicious), a
348
+ // nonexistent Go struct field, or (on an adapter whose runtime
349
+ // tolerates a missing key) a silent wrong render the moment a caller
350
+ // actually supplies a value. Parameter-destructuring aliases (`{ n:
351
+ // count }`) need no such entry — `sourceName`/`name` already handle
352
+ // that shape via `callerPropName` above.
353
+ for (const [local, callerKey] of resolveBodyDestructuredPropAliases(metadata.localConstants ?? [], metadata.propsObjectName)) {
354
+ if (local in out) continue
355
+ const origin = out[callerKey]
356
+ if (origin) out[local] = origin
357
+ }
358
+
314
359
  // Bare-props-arg safety net: the prop block above covers every prop
315
360
  // *declared* on the props type, but `propsParams` can miss props read
316
361
  // through an untyped / inline-typed `props` object. A signal / memo
package/src/types.ts CHANGED
@@ -404,18 +404,13 @@ export interface IRText {
404
404
  export interface IRExpression {
405
405
  type: 'expression'
406
406
  expr: string
407
- /** Pre-transformed expr with destructured prop refs rewritten to _p.xxx (for client JS templates). */
408
- templateExpr?: string
409
407
  /**
410
- * Marks a VALUE moved into element content by a lowering (today only
411
- * `lowerFormControlValueSsr`'s `<textarea>`, #2765) — client builders
412
- * must escape it, unlike the normal pre-rendered-HTML `${...}` case.
413
- *
414
- * Not read via `templateExpr` instead: that field also rebinds to
415
- * `_p.xxx`, which drops a destructured prop's `?? {}` default when the
416
- * builder's output lives in init scope (`client-js-generation.test.ts`).
408
+ * Pre-transformed expr with destructured prop refs rewritten to _p.xxx
409
+ * (for client JS templates). Carries ONLY that rewrite never an
410
+ * escaping wrapper: client-side escaping is decided at the splice
411
+ * (`ir-to-client-js/safe-html.ts`), not baked into IR text (#2795).
417
412
  */
418
- escapeInClientTemplate?: boolean
413
+ templateExpr?: string
419
414
  /**
420
415
  * Structured parse of `expr` (`parseExpression(expr.trim())`), attached once
421
416
  * during IR construction so SSR adapters emit from the tree instead of each
@@ -1,65 +0,0 @@
1
- /**
2
- * Text-slot HTML-escaping emit shape (#1694 + follow-up, #2651).
3
- *
4
- * Pins which interpolations the client template wraps in an escape call:
5
- * - a plain, non-conditional dynamic text slot (`{stringValue}`) IS
6
- * escaped — it becomes the slot's text content under `innerHTML`. Since
7
- * #2651 this goes through `escapeTextOrMarkup`, not bare `escapeText`:
8
- * the slot is claim-plan `kind: 'markup'` (the value may be a live
9
- * `Node`, or a `bfMarkup()`-branded JSX-element-prop value), and
10
- * `escapeTextOrMarkup` is a strict superset of `escapeText` for every
11
- * non-branded value — this pin's actual escaping behaviour for a plain
12
- * string is unchanged, only the call name changed to match the
13
- * REACTIVE side's existing `escapeTextOrNode` classification;
14
- * - a branch-slot expression (Child-position value inside a conditional
15
- * `template()` arrow) is routed through `__bfSlot` and must NOT be
16
- * wrapped in either escape call. `__bfSlot` returns raw
17
- * `<!--bf-slot:N-->` markers for live nodes; escaping the whole call
18
- * corrupts them and drops slotted content (the regression that broke
19
- * `e2e-site-ui`). `__bfSlot` escapes its own plain-string path
20
- * internally instead.
21
- */
22
-
23
- import { describe, test, expect } from 'bun:test'
24
- import { compileJSX } from '../compiler'
25
- import { TestAdapter } from '../adapters/test-adapter'
26
-
27
- const adapter = new TestAdapter()
28
-
29
- function getClientJs(source: string, filename: string): string {
30
- const result = compileJSX(source, filename, { adapter })
31
- expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
32
- const clientJs = result.files.find(f => f.type === 'clientJs')
33
- expect(clientJs).toBeDefined()
34
- return clientJs!.content
35
- }
36
-
37
- describe('text-slot escaping', () => {
38
- test('a plain text slot is wrapped in escapeTextOrMarkup (#2651)', () => {
39
- const clientJs = getClientJs(
40
- `'use client'
41
- export function Label({ text }: { text: string }) {
42
- return <span>{text}</span>
43
- }`,
44
- 'Label.tsx',
45
- )
46
- expect(clientJs).toMatch(/<!--bf:\w+-->\$\{escapeTextOrMarkup\(_p\.text\)\}<!--\/-->/)
47
- })
48
-
49
- test('a branch-slot expression is NOT wrapped in escapeTextOrMarkup or escapeText', () => {
50
- const clientJs = getClientJs(
51
- `'use client'
52
- import { createSignal } from '@barefootjs/client'
53
- export function Branch({ show }: { show: boolean }) {
54
- const [t] = createSignal('hi')
55
- return <div>{show ? <span>{t()}</span> : null}</div>
56
- }`,
57
- 'Branch.tsx',
58
- )
59
- // The branch value goes through __bfSlot (raw markers preserved)…
60
- expect(clientJs).toMatch(/\$\{__bfSlot\(/)
61
- // …and must never be double-wrapped by either text-escape call.
62
- expect(clientJs).not.toMatch(/escapeTextOrMarkup\(\s*__bfSlot/)
63
- expect(clientJs).not.toMatch(/escapeText\(\s*__bfSlot/)
64
- })
65
- })