@barefootjs/jsx 0.25.0 → 0.26.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 (41) hide show
  1. package/dist/adapters/dangerous-inner-html.d.ts +52 -24
  2. package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/index.js +248 -110
  5. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  6. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
  7. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
  9. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
  11. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
  13. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/reactivity.d.ts +24 -2
  16. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/types.d.ts +13 -0
  18. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  19. package/dist/to-locale-date-lowering.d.ts +31 -10
  20. package/dist/to-locale-date-lowering.d.ts.map +1 -1
  21. package/dist/types.d.ts +8 -4
  22. package/dist/types.d.ts.map +1 -1
  23. package/package.json +2 -2
  24. package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
  25. package/src/__tests__/nested-loop-conditional.test.ts +133 -0
  26. package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
  27. package/src/__tests__/reactive-factory-cross-file.test.ts +252 -1
  28. package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
  29. package/src/adapters/dangerous-inner-html.ts +101 -48
  30. package/src/analyzer.ts +222 -58
  31. package/src/ir-to-client-js/collect-elements.ts +28 -2
  32. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
  33. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
  34. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
  35. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
  36. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
  37. package/src/ir-to-client-js/reactivity.ts +61 -7
  38. package/src/ir-to-client-js/types.ts +13 -0
  39. package/src/rich-type-refusal.ts +3 -2
  40. package/src/to-locale-date-lowering.ts +50 -13
  41. package/src/types.ts +8 -4
@@ -1,30 +1,39 @@
1
1
  /**
2
2
  * Shared `dangerouslySetInnerHTML={{ __html: expr }}` recognition + policy
3
- * (#2207). Single place every template adapter calls from `renderElement`,
4
- * so the injection-safety-relevant policy "only a compile-time string
5
- * literal is lowered; anything else refuses loudly" lives in exactly one
6
- * reviewable spot instead of being re-derived independently in 8 adapters.
3
+ * (#2207, dynamic lowering #2319). Single place every template adapter calls
4
+ * from `renderElement`, so the injection-safety-relevant policy lives in
5
+ * exactly one reviewable spot instead of being re-derived independently in
6
+ * 8 adapters. `resolveDangerousInnerHtml` classifies the `__html` value into
7
+ * three cases the adapter then renders uniformly:
7
8
  *
8
- * Scope, deliberately narrow for v1 (see #2207 / #2215 for the follow-up):
9
- * a static literal is spliced directly into the adapter's OWN template
10
- * source as trusted text (same trust domain as hand-writing the HTML into
11
- * the template) never routed through a `|safe`/`|raw`/`{!! !!}`-style
12
- * runtime raw-output primitive, which would reopen a template-source
13
- * injection surface for no benefit (the value is already fully known at
14
- * compile time). A DYNAMIC value (signal, prop, a template literal WITH a
15
- * substitution, local const, anything non-literal) is refused with
16
- * `BF101` — a no-substitution template literal is NOT dynamic; the parser
17
- * normalizes it to the same `{kind:'literal', literalType:'string'}` shape
18
- * as a plain string literal, so it's treated identically (see
19
- * `staticHtmlLiteral` below). Hono/CSR already support it (the client
20
- * drives a `createEffect`-based `el.innerHTML = …`
21
- * assignment see `ir-to-client-js/emit-reactive.ts`), so this is a
22
- * template-adapter-only gap, tracked separately (#2215) rather than folded
23
- * into this literal-only cut.
9
+ * - `static` a compile-time string literal. Spliced directly into the
10
+ * adapter's OWN template source as trusted text (same trust domain as
11
+ * hand-writing the HTML into the template), guarded per-adapter against
12
+ * that language's template metacharacters (`dangerousInnerHtmlMetachar
13
+ * Violation`). NOT routed through a runtime raw-output primitive the
14
+ * value is fully known at compile time, so a runtime sink would reopen a
15
+ * template-source injection surface for no benefit.
16
+ *
17
+ * - `dynamic` — a prop-/signal-derived value (a signal read, prop, template
18
+ * literal WITH a substitution, local const, `??`-fallback, anything
19
+ * non-literal). The adapter serializes the `__html` expression via its own
20
+ * `convertExpressionTo<Lang>` and wraps the result in its runtime
21
+ * raw-output sink (Blade `{!! !!}`, ERB bare `<%= %>`, Go `template.HTML`,
22
+ * Jinja/MiniJinja `|safe`, Twig `|raw`, Mojolicious `<%== %>`, Xslate
23
+ * `mark_raw`). The runtime evaluates the expression at request time — the
24
+ * VALUE is never spliced into template source, so no metachar guard
25
+ * applies. This matches React's contract ("dangerously" = the caller owns
26
+ * the value's safety) and the Hono/CSR path, which already drives a
27
+ * `createEffect`-based `el.innerHTML = …` assignment (see
28
+ * `ir-to-client-js/emit-reactive.ts`).
29
+ *
30
+ * - `unlowerable` — the value is not a `{ __html: <expr> }` object literal
31
+ * at all (bare boolean/spread, or a variable holding the object). No
32
+ * `__html` expression to lower, so the adapter refuses with `BF101`.
24
33
  */
25
34
 
26
35
  import type { CompilerError, IRAttribute, IRElement, SourceLocation } from '../types.ts'
27
- import { parseExpression, type ParsedExpr } from '../expression-parser.ts'
36
+ import { parseExpression, stringifyParsedExpr, type ParsedExpr } from '../expression-parser.ts'
28
37
 
29
38
  const DANGEROUS_INNER_HTML_ATTR = 'dangerouslySetInnerHTML'
30
39
 
@@ -33,8 +42,28 @@ export function isDangerousInnerHtmlAttr(attr: IRAttribute): boolean {
33
42
  }
34
43
 
35
44
  export type DangerousInnerHtmlResolution =
45
+ // A compile-time string literal — spliced directly into the adapter's own
46
+ // template source as trusted text (guarded per-adapter against that
47
+ // language's template metacharacters). #2207.
36
48
  | { kind: 'static'; html: string }
37
- | { kind: 'dynamic'; expr: string; loc: SourceLocation }
49
+ // A dynamic (prop-/signal-derived) `__html` value. `valueParsed` is the
50
+ // inner expression's IR-parsed tree and `valueExpr` its re-stringified
51
+ // source — both accepted by every adapter's `convertExpressionTo<Lang>`,
52
+ // which serializes the expression so the adapter can wrap the result in
53
+ // its own runtime raw-output sink (Blade `{!! !!}`, ERB bare `<%= %>`,
54
+ // Go `template.HTML`, Jinja/MiniJinja `|safe`, Twig `|raw`, Mojolicious
55
+ // `<%== %>`, Xslate `mark_raw`). NO template-metacharacter guard applies:
56
+ // the value is evaluated at request time by the target runtime, never
57
+ // spliced into template source, so it cannot forge a template construct.
58
+ // This matches React semantics ("dangerously" = the caller owns the
59
+ // safety of the value) and the Hono/CSR path, which already drives a
60
+ // signal-reactive `el.innerHTML = …`. #2319 (successor to #2215).
61
+ | { kind: 'dynamic'; valueExpr: string; valueParsed: ParsedExpr; loc: SourceLocation }
62
+ // The attribute value is not a `{ __html: <expr> }` object literal at all
63
+ // (a bare boolean/spread, or a variable holding the object) — there is no
64
+ // `__html` expression to lower, so the adapter refuses with BF101. `expr`
65
+ // is the raw source for the diagnostic.
66
+ | { kind: 'unlowerable'; expr: string; loc: SourceLocation }
38
67
 
39
68
  /**
40
69
  * Resolve `element`'s `dangerouslySetInnerHTML` attribute, if present.
@@ -57,35 +86,44 @@ export function resolveDangerousInnerHtml(element: IRElement): DangerousInnerHtm
57
86
  if (!attr) return null
58
87
  if (attr.clientOnly) return null
59
88
  if (attr.value.kind !== 'expression') {
60
- return { kind: 'dynamic', expr: '', loc: attr.loc }
89
+ return { kind: 'unlowerable', expr: '', loc: attr.loc }
61
90
  }
62
91
  const parsed = attr.value.parsed ?? parseExpression(attr.value.expr.trim())
63
- const html = staticHtmlLiteral(parsed)
64
- if (html !== null) return { kind: 'static', html }
65
- return { kind: 'dynamic', expr: attr.value.expr, loc: attr.loc }
92
+ const value = htmlPropValue(parsed)
93
+ // Not a `{ __html: <expr> }` object literal — nothing to lower.
94
+ if (value === null) return { kind: 'unlowerable', expr: attr.value.expr, loc: attr.loc }
95
+ // A compile-time string literal (a quoted string or a no-substitution
96
+ // template literal, which the parser normalises to the same
97
+ // `{kind:'literal', literalType:'string'}` shape) is spliced as trusted
98
+ // template text; everything else is a dynamic value the adapter lowers
99
+ // through its raw-output sink.
100
+ if (value.kind === 'literal' && value.literalType === 'string') {
101
+ return { kind: 'static', html: value.value as string }
102
+ }
103
+ return {
104
+ kind: 'dynamic',
105
+ valueExpr: stringifyParsedExpr(value),
106
+ valueParsed: value,
107
+ loc: attr.loc,
108
+ }
66
109
  }
67
110
 
68
111
  /**
69
- * `{ __html: '<b>bold</b>' }` the literal string, nothing else. Exactly
70
- * one property, key `__html` (identifier or string form `{ __html: }`
71
- * and `{ '__html': … }` are equivalent JS), non-shorthand (`{ __html }`
72
- * would read a variable, not a literal), value a compile-time STRING
73
- * LITERAL `ParsedExpr` (`{kind:'literal', literalType:'string'}`) which
74
- * a no-substitution template literal (`` `<b>bold</b>` ``) already parses
75
- * to, so it's accepted the same as a quoted string. A template literal
76
- * WITH a substitution, a local `const`, string concatenation, or any other
77
- * non-literal shape all fall through to `null` — no const-folding, no
78
- * partial evaluation. Widening this is a single-place change here,
79
- * deliberately not attempted in v1 (#2207).
112
+ * The `__html` value expression of a `{ __html: <expr> }` object literal, or
113
+ * `null` when the parsed value is not that shape. Exactly one property, key
114
+ * `__html` (identifier or string form — `{ __html: … }` and `{ '__html': }`
115
+ * are equivalent JS). Shorthand `{ __html }` reads a variable of that name;
116
+ * it is a valid dynamic value (its `value` is the identifier `__html`), so it
117
+ * is admitted the same as `{ __html: __html }` — the caller then classifies
118
+ * literal-vs-dynamic. Spreads, computed keys, and multi-property objects fall
119
+ * through to `null` (the caller refuses them).
80
120
  */
81
- function staticHtmlLiteral(parsed: ParsedExpr): string | null {
121
+ function htmlPropValue(parsed: ParsedExpr): ParsedExpr | null {
82
122
  if (parsed.kind !== 'object-literal') return null
83
123
  if (parsed.properties.length !== 1) return null
84
124
  const [prop] = parsed.properties
85
- if (prop.shorthand) return null
86
125
  if (prop.key !== '__html') return null
87
- if (prop.value.kind !== 'literal' || prop.value.literalType !== 'string') return null
88
- return prop.value.value as string
126
+ return prop.value
89
127
  }
90
128
 
91
129
  /**
@@ -162,25 +200,40 @@ export function dangerousInnerHtmlMetacharViolation(html: string, adapterId: str
162
200
 
163
201
  /**
164
202
  * Purpose-built `BF101` for a `dangerouslySetInnerHTML` value this adapter
165
- * can't lower either genuinely dynamic (not a literal) or a static
166
- * literal that fails the metachar guard above. Named `reason` so both
167
- * refusal paths funnel through the same message shape rather than growing
168
- * two near-identical adapter-side error strings.
203
+ * can't lower. Two distinct failure families funnel through here, so the base
204
+ * message is chosen by whether a `reason` is supplied:
205
+ *
206
+ * - WITHOUT a `reason` — the value is not the required SHAPE: it must be an
207
+ * object literal with exactly one `__html` property (no spreads, extra keys,
208
+ * or computed keys). The message states that contract so a near-miss like
209
+ * `{ __html: x, extra: 1 }` (refused as `unlowerable`) doesn't get told it
210
+ * "expects an { __html: … }" it already appears to have.
211
+ * - WITH a `reason` — the value IS a well-formed `{ __html: … }` object
212
+ * literal that this adapter still can't lower: a static literal carrying
213
+ * template metacharacters (`dangerousInnerHtmlMetacharViolation`), or — on
214
+ * the Go adapter — a template-literal / conditional inner expression with no
215
+ * single-argument raw form. The `reason` is the real story; the base states
216
+ * only that the value can't be lowered here, not that the shape is wrong.
217
+ *
218
+ * A genuinely dynamic value with a lowerable inner expression no longer
219
+ * reaches here — it is lowered through the adapter's raw-output sink (#2319).
169
220
  */
170
221
  export function dangerousInnerHtmlDiagnostic(
171
222
  expr: string,
172
223
  loc: SourceLocation,
173
224
  reason?: string,
174
225
  ): CompilerError {
175
- const detail = reason ? ` — ${reason}.` : ''
226
+ const base = reason
227
+ ? `dangerouslySetInnerHTML value cannot be lowered on this adapter — ${reason}`
228
+ : 'dangerouslySetInnerHTML requires an object literal with a single `__html` property (e.g. { __html: value }) — spreads, extra keys, and computed keys are not supported'
176
229
  return {
177
230
  code: 'BF101',
178
231
  severity: 'error',
179
- message: `dangerouslySetInnerHTML requires a compile-time string literal __html value on template adapters (e.g. { __html: '...' })${expr ? `: ${expr.trim()}` : ''}${detail}`,
232
+ message: `${base}${expr ? `: ${expr.trim()}` : ''}`,
180
233
  loc,
181
234
  suggestion: {
182
235
  message:
183
- 'Dynamic or signal-derived HTML for dangerouslySetInnerHTML is only supported on Hono/CSR today (tracked separately: https://github.com/piconic-ai/barefootjs/issues/2215). Use an inline string literal, or defer it to the client with /* @client */ (e.g. dangerouslySetInnerHTML={/* @client */ { __html: expr }}) so hydration sets it instead of SSR.',
236
+ 'Pass an object literal { __html: value } with exactly one `__html` property (a string literal is spliced as trusted template text; a prop/signal value is lowered through the adapter\'s raw-output sink). To force it onto the client instead of SSR, use /* @client */ (e.g. dangerouslySetInnerHTML={/* @client */ { __html: expr }}) so hydration sets it.',
184
237
  },
185
238
  }
186
239
  }