@barefootjs/jsx 0.16.0 → 0.17.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 (57) hide show
  1. package/dist/adapters/env-signal.d.ts +38 -15
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  4. package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
  5. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  6. package/dist/analyzer-context.d.ts +29 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/builtin-lowering-plugins.d.ts +34 -0
  10. package/dist/builtin-lowering-plugins.d.ts.map +1 -0
  11. package/dist/expression-parser.d.ts +219 -163
  12. package/dist/expression-parser.d.ts.map +1 -1
  13. package/dist/index.d.ts +7 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6754 -6129
  16. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
  19. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  20. package/dist/jsx-to-ir.d.ts.map +1 -1
  21. package/dist/lowering-registry.d.ts +122 -0
  22. package/dist/lowering-registry.d.ts.map +1 -0
  23. package/dist/query-href-lowering.d.ts +63 -0
  24. package/dist/query-href-lowering.d.ts.map +1 -0
  25. package/dist/ssr-defaults.d.ts.map +1 -1
  26. package/dist/types.d.ts +169 -11
  27. package/dist/types.d.ts.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
  30. package/src/__tests__/analyzer.test.ts +53 -0
  31. package/src/__tests__/expression-parser.test.ts +703 -391
  32. package/src/__tests__/ir-reduce-op.test.ts +18 -21
  33. package/src/__tests__/ir-sort-comparator.test.ts +19 -20
  34. package/src/__tests__/lowering-registry.test.ts +141 -0
  35. package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
  36. package/src/__tests__/query-href-recognition.test.ts +58 -0
  37. package/src/__tests__/serialize-parsed-expr.test.ts +204 -0
  38. package/src/__tests__/unsupported-expression.test.ts +98 -4
  39. package/src/adapters/env-signal.ts +60 -21
  40. package/src/adapters/jsx-adapter.ts +17 -0
  41. package/src/adapters/parsed-expr-emitter.ts +39 -41
  42. package/src/analyzer-context.ts +72 -27
  43. package/src/analyzer.ts +226 -9
  44. package/src/builtin-lowering-plugins.ts +54 -0
  45. package/src/expression-parser.ts +1183 -927
  46. package/src/index.ts +26 -3
  47. package/src/ir-to-client-js/csr-substitute.ts +5 -0
  48. package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
  49. package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
  50. package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
  51. package/src/jsx-to-ir.ts +182 -43
  52. package/src/lowering-registry.ts +160 -0
  53. package/src/query-href-lowering.ts +147 -0
  54. package/src/ssr-defaults.ts +5 -1
  55. package/src/types.ts +171 -12
  56. package/src/__tests__/flatmap-support.test.ts +0 -218
  57. package/src/__tests__/reduce-op.test.ts +0 -201
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Backend-neutral destructuring of a recognised `queryHref(base, { … })` call
3
+ * (#2042) into a base expression plus include triples, shared by the SSR
4
+ * adapters' query lowering.
5
+ *
6
+ * `queryHref` is the pure functional URL-query builder (the counterpart to
7
+ * `searchParams()`); its call + object literal are already structured IR, so an
8
+ * adapter lowers it to its query helper without any block-body recognition or
9
+ * re-parse. This module only does the structural match — turning the object
10
+ * literal's properties into `{ guard, key, value }` triples — leaving each
11
+ * adapter to format the include condition and the helper call in its own
12
+ * template language.
13
+ *
14
+ * Inclusion is truthy-omit over string values (matching the client `queryHref`'s
15
+ * `if (value)`): a plain `key: v` is included iff `v` is a non-empty string
16
+ * (`guard: null`); a conditional `key: cond ? a : <undefined|null|''>` is
17
+ * included iff `cond` AND `a` is non-empty (`guard: cond`, `value: a`).
18
+ */
19
+
20
+ import type { ParsedExpr } from './expression-parser.ts'
21
+
22
+ export interface QueryHrefTriple {
23
+ /**
24
+ * The conditional test of a `key: cond ? a : <omit>` include, or null for a
25
+ * plain `key: v` (which is included purely on value-truthiness). An adapter
26
+ * combines this with the value's non-emptiness to form the include condition.
27
+ */
28
+ guard: ParsedExpr | null
29
+ /** The literal search-param key. */
30
+ key: string
31
+ /** The value expression (the consequent for a conditional include). */
32
+ value: ParsedExpr
33
+ }
34
+
35
+ export interface QueryHrefCall {
36
+ base: ParsedExpr
37
+ triples: QueryHrefTriple[]
38
+ }
39
+
40
+ /**
41
+ * Match a `queryHref(base, { … })` call from its callee + args, returning the
42
+ * base and include triples, or null when it isn't a `queryHref` call with a
43
+ * plain object-literal second argument (→ the adapter falls back to its generic
44
+ * lowering). `localNames` are the bindings `queryHref` is imported under (from
45
+ * `queryHrefLocalNames`).
46
+ */
47
+ export function matchQueryHrefCall(
48
+ callee: ParsedExpr,
49
+ args: readonly ParsedExpr[],
50
+ localNames: ReadonlySet<string>,
51
+ ): QueryHrefCall | null {
52
+ if (callee.kind !== 'identifier' || !localNames.has(callee.name)) return null
53
+ if (args.length !== 2) return null
54
+ const [base, obj] = args
55
+ // A dynamic (non-literal) params object can't be lowered to static include
56
+ // triples — fall back to the generic lowering.
57
+ if (obj.kind !== 'object-literal') return null
58
+
59
+ const triples: QueryHrefTriple[] = []
60
+ for (const p of obj.properties) {
61
+ const v = p.value
62
+ if (v.kind === 'conditional' && isOmitBranch(v.alternate)) {
63
+ triples.push({ guard: v.test, key: p.key, value: v.consequent })
64
+ } else {
65
+ triples.push({ guard: null, key: p.key, value: v })
66
+ }
67
+ }
68
+ return { base, triples }
69
+ }
70
+
71
+ /**
72
+ * Format a {@link QueryHrefCall} as the flat argument list for a guard-list
73
+ * query helper (`bf->query(base, guard, key, value, …)` in Mojo / `$bf.query(…)`
74
+ * in Xslate — the two adapters whose helper does the non-empty check itself).
75
+ * Each triple contributes a guard (`'1'` for a plain include, or the lowered
76
+ * condition for a conditional one), the key as a string literal, and the value —
77
+ * all lowered through the adapter's `emit`. The caller wraps the result in its
78
+ * own `<helper>(…)` call. (The go-template adapter folds the non-empty check
79
+ * into the include condition itself, so it formats its own form instead.)
80
+ *
81
+ * A conditional guard that is NOT already boolean-shaped (a bare value, a member
82
+ * access, `&&`/`||`) is JS *string* truthiness — `'0'` is a truthy string in JS
83
+ * but false under Perl's `unless`. To keep SSR byte-identical to the client (and
84
+ * to the go adapter, whose `lowerUrlGuard` does the same), such a guard is
85
+ * normalised to a `guard !== ''` test, emitted against a string literal so each
86
+ * adapter renders string `ne`, not numeric `!=`. Comparisons / `!x` / boolean
87
+ * literals already yield a real boolean and pass through unchanged.
88
+ */
89
+ export function queryHrefArgs(q: QueryHrefCall, emit: (e: ParsedExpr) => string): string[] {
90
+ const out = [emit(q.base)]
91
+ for (const t of q.triples) {
92
+ if (t.guard === null) {
93
+ out.push('1')
94
+ } else if (isBoolShapeGuard(t.guard)) {
95
+ out.push(`(${emit(t.guard)})`)
96
+ } else {
97
+ const test: ParsedExpr = {
98
+ kind: 'binary',
99
+ op: '!==',
100
+ left: t.guard,
101
+ right: { kind: 'literal', value: '', literalType: 'string' },
102
+ }
103
+ out.push(`(${emit(test)})`)
104
+ }
105
+ out.push(emit({ kind: 'literal', value: t.key, literalType: 'string' }))
106
+ out.push(emit(t.value))
107
+ }
108
+ return out
109
+ }
110
+
111
+ const GUARD_BOOL_OPS: ReadonlySet<string> = new Set([
112
+ '==',
113
+ '===',
114
+ '!=',
115
+ '!==',
116
+ '<',
117
+ '>',
118
+ '<=',
119
+ '>=',
120
+ ])
121
+
122
+ /**
123
+ * Whether a conditional-include guard already evaluates to a real boolean, so it
124
+ * can be emitted as-is rather than wrapped in a `!== ''` string-truthiness test.
125
+ * A comparison, a `!negation`, or a boolean literal qualifies; a bare value /
126
+ * member / `&&` / `||` does not. Mirrors the go adapter's `lowerUrlGuard`
127
+ * `isBoolShape` so the four backends agree on which guards need normalising.
128
+ */
129
+ function isBoolShapeGuard(g: ParsedExpr): boolean {
130
+ return (
131
+ (g.kind === 'binary' && GUARD_BOOL_OPS.has(g.op)) ||
132
+ (g.kind === 'unary' && g.op === '!') ||
133
+ (g.kind === 'literal' && g.literalType === 'boolean')
134
+ )
135
+ }
136
+
137
+ /**
138
+ * The falsy "omit" branch of a conditional include — `undefined` (an identifier),
139
+ * `null`, or `''` — which makes `cond ? v : <omit>` a conditional include.
140
+ */
141
+ function isOmitBranch(node: ParsedExpr): boolean {
142
+ if (node.kind === 'identifier') return node.name === 'undefined'
143
+ if (node.kind === 'literal') {
144
+ return node.literalType === 'null' || (node.literalType === 'string' && node.value === '')
145
+ }
146
+ return false
147
+ }
@@ -143,6 +143,10 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
143
143
  }
144
144
  for (const sig of metadata.signals) {
145
145
  if (!sig.getter || sig.isModule) continue
146
+ // Env signals (#2057) have no static SSR default — their value is the
147
+ // request-scoped reader, seeded by the adapter's env-signal binding, not a
148
+ // baked initial value.
149
+ if (sig.envReader) continue
146
150
  const value = tryStaticEval(sig.initialValue, { bindings, propsLike })
147
151
  out[sig.getter] = { value: resultToJsonable(value) }
148
152
  bindings[sig.getter] = value
@@ -169,7 +173,7 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
169
173
  if (metadata.propsObjectName !== null) {
170
174
  const referenced = new Set<string>()
171
175
  for (const sig of metadata.signals) {
172
- if (!sig.getter || sig.isModule) continue
176
+ if (!sig.getter || sig.isModule || sig.envReader) continue
173
177
  collectPropRefs(sig.initialValue, metadata.propsObjectName, referenced)
174
178
  }
175
179
  for (const memo of metadata.memos) {
package/src/types.ts CHANGED
@@ -4,7 +4,25 @@
4
4
  * JSX-independent intermediate representation for multi-backend support.
5
5
  */
6
6
 
7
- import type { ParsedExpr, ParsedStatement, SortComparator } from './expression-parser.ts'
7
+ import type { ParsedExpr, ParsedStatement } from './expression-parser.ts'
8
+
9
+ /**
10
+ * Loop-hoisted sort comparator for the `.sort().map()` / `.toSorted().map()`
11
+ * pattern (#2018 P5). Carries the generic comparator `arrow` (params + body)
12
+ * that the SSR adapter serializes to the runtime evaluator (eval-first) or, for
13
+ * a `localeCompare` comparator the evaluator can't model, recovers a structured
14
+ * comparator from via `sortComparatorFromArrow`. The `paramA` / `paramB` / `raw`
15
+ * fields round-trip the comparator to native JS for the client / CSR path
16
+ * (`(paramA, paramB) => raw`), so the client is untouched.
17
+ */
18
+ export type IRLoopSort = {
19
+ // Always the comparator arrow itself — narrowed so consumers read
20
+ // `.params` / `.body` without a defensive `kind` check or non-null assertion.
21
+ arrow: Extract<ParsedExpr, { kind: 'arrow' }>
22
+ paramA: string
23
+ paramB: string
24
+ raw: string
25
+ }
8
26
 
9
27
  // =============================================================================
10
28
  // Source Location (for Error Reporting)
@@ -323,6 +341,20 @@ export interface IRExpression {
323
341
  expr: string
324
342
  /** Pre-transformed expr with destructured prop refs rewritten to _p.xxx (for client JS templates). */
325
343
  templateExpr?: string
344
+ /**
345
+ * Structured parse of `expr` (`parseExpression(expr.trim())`), attached once
346
+ * during IR construction so SSR adapters emit from the tree instead of each
347
+ * re-parsing the string at emit time (and so a multi-adapter build parses it
348
+ * once, not per adapter). Plain serializable data.
349
+ *
350
+ * OPTIONAL by design — consumers MUST fall back to parsing `expr` when it is
351
+ * missing. It is absent for an empty/whitespace `expr`, and may also be
352
+ * absent for a node the IR-build walk doesn't reach (the walk is best-effort;
353
+ * under-coverage is a missed optimization, never a behavioural change). When
354
+ * present, an unparsable expression is a `{ kind: 'unsupported' }` node (the
355
+ * adapter's own support gate handles it).
356
+ */
357
+ parsed?: ParsedExpr
326
358
  typeInfo: TypeInfo | null
327
359
  reactive: boolean
328
360
  slotId: string | null
@@ -347,6 +379,13 @@ export interface IRConditional {
347
379
  condition: string
348
380
  /** Pre-transformed condition with destructured prop refs rewritten to _p.xxx. */
349
381
  templateCondition?: string
382
+ /**
383
+ * Structured parse of `condition` (`parseExpression(condition.trim())`),
384
+ * attached during IR construction so adapters lower the condition from the
385
+ * tree instead of re-parsing the string. Optional/best-effort — see
386
+ * `IRExpression.parsed`; consumers fall back to parsing `condition`.
387
+ */
388
+ parsedCondition?: ParsedExpr
350
389
  conditionType: TypeInfo | null
351
390
  reactive: boolean
352
391
  whenTrue: IRNode
@@ -404,6 +443,14 @@ export interface IRLoop {
404
443
  */
405
444
  method?: 'flatMap'
406
445
  array: string
446
+ /**
447
+ * Structured parse of `array` (`parseExpression(array.trim())`), attached
448
+ * during IR construction so adapters lower the loop's array from the tree
449
+ * instead of re-parsing the string (e.g. the Go adapter's scalar-literal
450
+ * loop typing). Optional/best-effort — mirrors `IRExpression.parsed`;
451
+ * consumers fall back to parsing `array`.
452
+ */
453
+ arrayParsed?: ParsedExpr
407
454
  /** Pre-transformed array expr with destructured prop refs rewritten to _p.xxx. */
408
455
  templateArray?: string
409
456
  arrayType: TypeInfo | null
@@ -471,14 +518,16 @@ export interface IRLoop {
471
518
  * When present, the loop renders with an if-condition wrapping each iteration.
472
519
  * Example: todos.filter(t => !t.done).map(...) stores { param: 't', predicate: ParsedExpr, raw: '!t.done' }
473
520
  *
474
- * For block-body filters like:
521
+ * Block-body filters like
475
522
  * filter(t => { const f = filter(); if (f === 'active') return !t.done; return true })
476
- * The blockBody field contains the parsed statements.
523
+ * are normalized to a single boolean `predicate` expression at IR-build time
524
+ * (#2040, `foldBlockToExpr` + `predicateTernaryToLogical` in `jsx-to-ir`), so
525
+ * adapters only ever see the unified expression form — there is no separate
526
+ * block-statement shape to lower.
477
527
  */
478
528
  filterPredicate?: {
479
529
  param: string
480
- predicate?: ParsedExpr // Expression body
481
- blockBody?: ParsedStatement[] // Block body (mutually exclusive with predicate)
530
+ predicate?: ParsedExpr // Boolean predicate expression (folded from any block body)
482
531
  raw: string // Original string for error messages
483
532
  }
484
533
 
@@ -487,14 +536,14 @@ export interface IRLoop {
487
536
  * When present, the loop array is sorted before iteration.
488
537
  * Example: todos.sort((a, b) => a.priority - b.priority).map(...)
489
538
  *
490
- * The structured shape carries enough info for both adapters to
491
- * emit the same `bf_sort` / `bf->sort` call (`SortComparator` is
492
- * defined in `expression-parser.ts` because the standalone
493
- * `array-method` IR variant uses the same type). The loop-hoist
494
- * path lifts a comparator off a sibling `array-method` node
495
- * during `jsx-to-ir.ts` chain detection — see `extractSortComparator`.
539
+ * The {@link IRLoopSort} struct carries the generic comparator `arrow`
540
+ * (params + body) the SSR adapter serializes to the runtime evaluator
541
+ * (eval-first; `sortComparatorFromArrow` fallback for `localeCompare`), plus
542
+ * the param names + raw body for the client JS round-trip. Lifted off the
543
+ * `.sort()` callback during `jsx-to-ir.ts` chain detection see
544
+ * `extractSortComparator`. (#2018 P5)
496
545
  */
497
- sortComparator?: SortComparator
546
+ sortComparator?: IRLoopSort
498
547
 
499
548
  /**
500
549
  * When both filter and sort are chained, indicates the order of operations.
@@ -746,6 +795,13 @@ export interface IRIfStatement {
746
795
  condition: string
747
796
  /** Pre-transformed condition with destructured prop refs rewritten to _p.xxx. */
748
797
  templateCondition?: string
798
+ /**
799
+ * Structured parse of `condition` (`parseExpression(condition.trim())`),
800
+ * attached during IR construction so adapters lower the condition from the
801
+ * tree instead of re-parsing the string. Optional/best-effort — see
802
+ * `IRExpression.parsed`; consumers fall back to parsing `condition`.
803
+ */
804
+ parsedCondition?: ParsedExpr
749
805
  /** The JSX return in the then branch */
750
806
  consequent: IRNode
751
807
  /** The else branch: either another IRIfStatement (else if) or IRNode (final else) */
@@ -839,6 +895,14 @@ export interface ExpressionAttr {
839
895
  /** `expr` with destructured prop refs rewritten to `_p.xxx`, for SSR
840
896
  * template inlining. Absent when no rewrite was needed. */
841
897
  templateExpr?: string
898
+ /**
899
+ * Structured parse of `expr` (`parseExpression(expr.trim())`), attached
900
+ * during IR construction so adapters lower the attribute value from the tree
901
+ * instead of re-parsing the string (often several times per attribute).
902
+ * Optional/best-effort — see `IRExpression.parsed`; consumers fall back to
903
+ * parsing `expr`.
904
+ */
905
+ parsed?: ParsedExpr
842
906
  /** Set when the producer peeled an `expr || undefined` boolean-presence
843
907
  * pattern; adapters fold this back into `(expr) || undefined` at emit. */
844
908
  presenceOrUndefined?: boolean
@@ -873,6 +937,16 @@ export interface SpreadAttr {
873
937
  kind: 'spread'
874
938
  expr: string
875
939
  templateExpr?: string
940
+ /**
941
+ * Structured parse of `expr` (`parseExpression(expr.trim())`), attached
942
+ * during IR construction so adapters lower the spread bag from the tree
943
+ * instead of re-parsing the string with `ts.createSourceFile`. Optional /
944
+ * best-effort — mirrors `ExpressionAttr.parsed`: it may be absent (a node the
945
+ * attach walk misses, or an empty `expr`), and parsing may yield
946
+ * `{ kind: 'unsupported' }`, which adapters treat as unlowerable and handle
947
+ * via their existing non-conditional spread paths (or BF101).
948
+ */
949
+ parsed?: ParsedExpr
876
950
  /**
877
951
  * Component-scoped, stable slot ID assigned at IR-build time for
878
952
  * adapters that need to plumb the spread bag through a structured
@@ -1039,6 +1113,14 @@ export interface SignalInfo {
1039
1113
  getter: string
1040
1114
  setter: string | null
1041
1115
  initialValue: string
1116
+ /**
1117
+ * `initialValue` parsed into a structured tree (Roadmap A). Attached
1118
+ * best-effort by the analyzer so adapters can lower a literal initial value
1119
+ * (e.g. `useState(['a', 'b'])`) from structure instead of re-parsing the
1120
+ * string with `ts.createSourceFile`. Absent when the shape isn't supported;
1121
+ * consumers fall back to parsing `initialValue`.
1122
+ */
1123
+ parsed?: ParsedExpr
1042
1124
  /** Initial value with TypeScript type annotations preserved, for .tsx output */
1043
1125
  typedInitialValue?: string
1044
1126
  type: TypeInfo
@@ -1075,6 +1157,26 @@ export interface SignalInfo {
1075
1157
  isModule?: boolean
1076
1158
  /** When true, the declaration carries an `export` keyword. */
1077
1159
  isExported?: boolean
1160
+ /**
1161
+ * Request-scoped environment-signal key when this signal was produced by an
1162
+ * env-signal factory (`createSearchParams()` → `'search'`), rather than by
1163
+ * `createSignal`. Set structurally by the analyzer (#2057) — the getter is a
1164
+ * normal reactive getter (so it lands in the fold purity oracle for free, no
1165
+ * name allow-list), but its *value* is a request-scoped reader with methods
1166
+ * (`.get(key)`), which adapters lower to their per-request reader object
1167
+ * instead of a plain template field. This flag is how adapters recognise an
1168
+ * env signal from structure instead of matching the import name.
1169
+ */
1170
+ envReader?: string
1171
+ /**
1172
+ * For an env signal (`envReader` set), the exact callee text of its factory
1173
+ * call as written — `'createSearchParams'`, an alias (`'csp'` for
1174
+ * `import { createSearchParams as csp }`), or a namespace access
1175
+ * (`'bf.createSearchParams'`). Backends that re-emit the declaration (client
1176
+ * JS, JSX/Hono SSR) emit `<envFactory>()` so the call resolves to the binding
1177
+ * actually in scope, not a hardcoded canonical name (#2057).
1178
+ */
1179
+ envFactory?: string
1078
1180
  }
1079
1181
 
1080
1182
  export interface MemoInfo {
@@ -1082,6 +1184,52 @@ export interface MemoInfo {
1082
1184
  computation: string
1083
1185
  /** Computation with TypeScript type annotations preserved, for .tsx output */
1084
1186
  typedComputation?: string
1187
+ /**
1188
+ * Structured parse of the memo's BODY as a single value expression, computed
1189
+ * once at analysis time. Lets adapters pattern-match the memo's shape on the
1190
+ * structured tree instead of re-parsing `computation` with their own AST walks
1191
+ * / regexes.
1192
+ *
1193
+ * Set for an expression-bodied arrow (`() => <body>`) whose body
1194
+ * `parseExpression` supports, AND — since #2040 — for a block-bodied memo
1195
+ * (`() => { … }`) whose statements `foldBlockToExpr` can normalize to one
1196
+ * expression (`let`-inline + value `if` / early `return` → ternary). A block
1197
+ * the fold refuses (imperative residue) or a shape `parseExpression` can't
1198
+ * represent leaves `parsed` undefined, so consumers must still fall back to
1199
+ * `parsedBlock` / `computation` when it's missing. NOTE: a present `parsed`
1200
+ * therefore no longer implies an expression-bodied arrow.
1201
+ */
1202
+ parsed?: ParsedExpr
1203
+ /**
1204
+ * Whether the memo's effective body is a template literal (`() => `…`` or a
1205
+ * block body whose first `return` is one), classified once at analysis time
1206
+ * from the real arrow AST. Lets the Go adapter pick the `string` field type
1207
+ * without re-parsing `computation` with `ts.createSourceFile`. A template
1208
+ * literal — including a no-substitution `` `plain` `` — folds to a plain
1209
+ * string `ParsedExpr` literal, losing the backtick distinction, so this is a
1210
+ * dedicated flag rather than a `parsed.kind` check.
1211
+ */
1212
+ bodyIsTemplateLiteral?: boolean
1213
+ /**
1214
+ * A block-bodied memo's statements, parsed best-effort (tolerant: a statement
1215
+ * the parser can't represent is omitted). Lets the Go adapter pattern-match
1216
+ * block-body memo shapes — e.g. the `const k = getter(); if (!k) return CONST`
1217
+ * guard — on the structured statements instead of re-parsing `computation`
1218
+ * with `ts.createSourceFile`. Absent for expression-bodied memos (those carry
1219
+ * `parsed` instead) and when the arrow has no block body.
1220
+ */
1221
+ parsedBlock?: ParsedStatement[]
1222
+ /**
1223
+ * Whether {@link parsedBlock} represents EVERY statement of the block (true)
1224
+ * or the tolerant parser omitted at least one it couldn't represent (false).
1225
+ * A consumer that must reason about the whole block — e.g. one that bails on
1226
+ * any statement it doesn't recognise (the template-literal memo lowering) —
1227
+ * reads this and falls back when it's `false`, since omitted statements are
1228
+ * otherwise invisible. Consumers that scan for a recognised prefix and ignore
1229
+ * the rest (the guard-and-return-const lowering) can disregard it. Only set
1230
+ * when `parsedBlock` is.
1231
+ */
1232
+ parsedBlockComplete?: boolean
1085
1233
  type: TypeInfo
1086
1234
  deps: string[]
1087
1235
  loc: SourceLocation
@@ -1282,6 +1430,17 @@ export interface FunctionInfo {
1282
1430
  export interface ConstantInfo {
1283
1431
  name: string
1284
1432
  value?: string
1433
+ /**
1434
+ * `value` parsed into a structured tree (Roadmap A). Attached best-effort by
1435
+ * the analyzer (parsed from the parenthesised value so a bare object literal
1436
+ * — which TS reads as a block at statement position — resolves to an
1437
+ * `object-literal` rather than failing). Lets adapters lower a constant value
1438
+ * (e.g. a module-scope record's `{ … }`) from structure instead of
1439
+ * re-parsing the string with `ts.createSourceFile`. Absent when the constant
1440
+ * has no `value` string (e.g. an inlined-JSX const) or when the analyzer
1441
+ * couldn't structure it (best-effort — consumers fall back to the string).
1442
+ */
1443
+ parsed?: ParsedExpr
1285
1444
  /** Value with TypeScript type annotations preserved, for .tsx output */
1286
1445
  typedValue?: string
1287
1446
  valueBranches?: string[]
@@ -1,218 +0,0 @@
1
- /**
2
- * BarefootJS Compiler - .flatMap() support (#1554 / #1448 Tier C)
3
- *
4
- * flatMap callbacks containing JSX must compile to valid JS for
5
- * JavaScript runtime adapters (Hono, CSR). Template-language adapters
6
- * (Go, Mojo) do not support flatMap — the workaround is .map() + Fragment.
7
- */
8
-
9
- import { describe, test, expect } from 'bun:test'
10
- import { compileJSX } from '../compiler'
11
- import { TestAdapter } from '../adapters/test-adapter'
12
- import { HonoAdapter } from '../../../../packages/adapter-hono/src/adapter/hono-adapter'
13
-
14
- const adapter = new TestAdapter()
15
-
16
- describe('.flatMap() — simple arrow with array literal', () => {
17
- test('arrow body returning [<A/>, <B/>] compiles to flatMap with valid JS', () => {
18
- const source = `
19
- 'use client'
20
-
21
- export function DL(props: { items: { term: string; def: string }[] }) {
22
- return (
23
- <dl>
24
- {props.items.flatMap((item, i) => [
25
- <dt key={\`dt-\${i}\`}>{item.term}</dt>,
26
- <dd key={\`dd-\${i}\`}>{item.def}</dd>
27
- ])}
28
- </dl>
29
- )
30
- }
31
- `
32
- const result = compileJSX(source, 'DL.tsx', { adapter })
33
- expect(result.errors).toHaveLength(0)
34
-
35
- const clientJs = result.files.find(f => f.type === 'clientJs')!
36
- expect(clientJs.content).toContain('.flatMap(')
37
- expect(clientJs.content).toContain('.join(')
38
- // Template contains compiled HTML (not raw JSX)
39
- expect(clientJs.content).toContain('item.term')
40
- expect(clientJs.content).toContain('item.def')
41
- })
42
- })
43
-
44
- describe('.flatMap() — complex block body with conditional returns', () => {
45
- test('block body with variable JSX and conditional returns produces valid client JS', () => {
46
- const source = `
47
- 'use client'
48
-
49
- export function Timeline(props: { frames: { label: string }[] }) {
50
- return (
51
- <div>
52
- {props.frames.flatMap((frame, i) => {
53
- const panel = (
54
- <ResizablePanel key={\`p-\${i}\`}>
55
- {i + 1}
56
- </ResizablePanel>
57
- )
58
- if (i === 0) return [panel]
59
- return [<ResizableHandle key={\`h-\${i}\`} />, panel]
60
- })}
61
- </div>
62
- )
63
- }
64
- `
65
- const result = compileJSX(source, 'Timeline.tsx', { adapter })
66
- expect(result.errors).toHaveLength(0)
67
-
68
- const clientJs = result.files.find(f => f.type === 'clientJs')!
69
- expect(clientJs.content).toContain('.flatMap(')
70
- expect(clientJs.content).toContain('.join(')
71
- // JSX should be compiled to renderChild calls, not raw JSX
72
- expect(clientJs.content).toContain("renderChild('ResizablePanel'")
73
- expect(clientJs.content).toContain("renderChild('ResizableHandle'")
74
- expect(clientJs.content).not.toContain('<ResizablePanel')
75
- expect(clientJs.content).not.toContain('<ResizableHandle')
76
- })
77
- })
78
-
79
- describe('.flatMap() — Hono adapter', () => {
80
- test('Hono preserves JSX in flatMap callback', () => {
81
- const source = `
82
- 'use client'
83
-
84
- export function Timeline(props: { frames: { label: string }[] }) {
85
- return (
86
- <div>
87
- {props.frames.flatMap((frame, i) => {
88
- const panel = (
89
- <ResizablePanel key={\`p-\${i}\`}>
90
- {i + 1}
91
- </ResizablePanel>
92
- )
93
- if (i === 0) return [panel]
94
- return [<ResizableHandle key={\`h-\${i}\`} />, panel]
95
- })}
96
- </div>
97
- )
98
- }
99
- `
100
- const result = compileJSX(source, 'Timeline.tsx', { adapter: new HonoAdapter() })
101
- expect(result.errors).toHaveLength(0)
102
-
103
- const markedTemplate = result.files.find(f => f.type === 'markedTemplate')!
104
- // Hono should use flatMap (not map)
105
- expect(markedTemplate.content).toContain('.flatMap(')
106
- // Hono preserves JSX natively
107
- expect(markedTemplate.content).toContain('<ResizablePanel')
108
- expect(markedTemplate.content).toContain('<ResizableHandle')
109
- })
110
-
111
- test('simple flatMap arrow with array literal works in Hono', () => {
112
- const source = `
113
- 'use client'
114
-
115
- export function DL(props: { items: { term: string; def: string }[] }) {
116
- return (
117
- <dl>
118
- {props.items.flatMap((item, i) => [
119
- <dt key={\`dt-\${i}\`}>{item.term}</dt>,
120
- <dd key={\`dd-\${i}\`}>{item.def}</dd>
121
- ])}
122
- </dl>
123
- )
124
- }
125
- `
126
- const result = compileJSX(source, 'DL.tsx', { adapter: new HonoAdapter() })
127
- expect(result.errors).toHaveLength(0)
128
-
129
- const markedTemplate = result.files.find(f => f.type === 'markedTemplate')!
130
- expect(markedTemplate.content).toContain('.flatMap(')
131
- })
132
- })
133
-
134
- describe('.flatMap() — single JSX return (same as map)', () => {
135
- test('flatMap with single JSX return compiles like map', () => {
136
- const source = `
137
- 'use client'
138
-
139
- export function List(props: { items: string[] }) {
140
- return (
141
- <ul>
142
- {props.items.flatMap((item, i) => (
143
- <li key={i}>{item}</li>
144
- ))}
145
- </ul>
146
- )
147
- }
148
- `
149
- const result = compileJSX(source, 'List.tsx', { adapter })
150
- expect(result.errors).toHaveLength(0)
151
-
152
- const clientJs = result.files.find(f => f.type === 'clientJs')!
153
- expect(clientJs.content).toContain('.flatMap(')
154
- // Template contains compiled HTML template literal, not raw JSX
155
- expect(clientJs.content).toContain('data-key')
156
- })
157
- })
158
-
159
- describe('.flatMap() — variable-assigned result (#1554 comment)', () => {
160
- test('flatMap stored in const, then used in JSX, compiles without raw JSX', () => {
161
- const source = `
162
- 'use client'
163
-
164
- export function TimelineBar(props: { items: string[] }) {
165
- const children = props.items.flatMap((item, i) => {
166
- const panel = (
167
- <ResizablePanel key={item} defaultSize={50} className="segment">
168
- <span>{i + 1}</span>
169
- </ResizablePanel>
170
- )
171
- if (i === 0) return [panel]
172
- return [<ResizableHandle key={\`h-\${item}\`} />, panel]
173
- })
174
-
175
- return (
176
- <ResizablePanelGroup direction="horizontal">
177
- {children}
178
- </ResizablePanelGroup>
179
- )
180
- }
181
- `
182
- const result = compileJSX(source, 'TimelineBar.tsx', { adapter })
183
- expect(result.errors).toHaveLength(0)
184
-
185
- const clientJs = result.files.find(f => f.type === 'clientJs')!
186
- // flatMap should be used (not map)
187
- expect(clientJs.content).toContain('.flatMap(')
188
- // JSX should be compiled to renderChild calls, not raw JSX
189
- expect(clientJs.content).toContain("renderChild('ResizablePanel'")
190
- expect(clientJs.content).toContain("renderChild('ResizableHandle'")
191
- expect(clientJs.content).not.toContain('<ResizablePanel')
192
- expect(clientJs.content).not.toContain('<ResizableHandle')
193
- // The raw const declaration should not appear in the init function
194
- expect(clientJs.content).not.toContain('const children = ')
195
- })
196
-
197
- test('map() stored in const with JSX also gets inlined', () => {
198
- const source = `
199
- 'use client'
200
-
201
- export function List(props: { items: string[] }) {
202
- const rendered = props.items.map((item, i) => (
203
- <ListItem key={i} label={item} />
204
- ))
205
-
206
- return (
207
- <ul>{rendered}</ul>
208
- )
209
- }
210
- `
211
- const result = compileJSX(source, 'List.tsx', { adapter })
212
- expect(result.errors).toHaveLength(0)
213
-
214
- const clientJs = result.files.find(f => f.type === 'clientJs')!
215
- expect(clientJs.content).not.toContain('<ListItem')
216
- expect(clientJs.content).not.toContain('const rendered = ')
217
- })
218
- })