@barefootjs/jsx 0.15.2 → 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.
- package/dist/adapters/env-signal.d.ts +38 -15
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +29 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts +34 -0
- package/dist/builtin-lowering-plugins.d.ts.map +1 -0
- package/dist/expression-parser.d.ts +219 -163
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6892 -6118
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +122 -0
- package/dist/lowering-registry.d.ts.map +1 -0
- package/dist/profiler.d.ts +115 -0
- package/dist/profiler.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts +63 -0
- package/dist/query-href-lowering.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +169 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
- package/src/__tests__/analyzer.test.ts +53 -0
- package/src/__tests__/expression-parser.test.ts +703 -391
- package/src/__tests__/ir-reduce-op.test.ts +18 -21
- package/src/__tests__/ir-sort-comparator.test.ts +19 -20
- package/src/__tests__/lowering-registry.test.ts +141 -0
- package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
- package/src/__tests__/profiler.test.ts +149 -0
- package/src/__tests__/query-href-recognition.test.ts +58 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +204 -0
- package/src/__tests__/unsupported-expression.test.ts +98 -4
- package/src/adapters/env-signal.ts +60 -21
- package/src/adapters/jsx-adapter.ts +17 -0
- package/src/adapters/parsed-expr-emitter.ts +39 -41
- package/src/analyzer-context.ts +72 -27
- package/src/analyzer.ts +226 -9
- package/src/builtin-lowering-plugins.ts +54 -0
- package/src/expression-parser.ts +1183 -927
- package/src/index.ts +35 -3
- package/src/ir-to-client-js/csr-substitute.ts +5 -0
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
- package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
- package/src/jsx-to-ir.ts +182 -43
- package/src/lowering-registry.ts +160 -0
- package/src/profiler.ts +328 -0
- package/src/query-href-lowering.ts +147 -0
- package/src/ssr-defaults.ts +5 -1
- package/src/types.ts +171 -12
- package/src/__tests__/flatmap-support.test.ts +0 -218
- package/src/__tests__/reduce-op.test.ts +0 -201
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
|
|
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
|
-
*
|
|
521
|
+
* Block-body filters like
|
|
475
522
|
* filter(t => { const f = filter(); if (f === 'active') return !t.done; return true })
|
|
476
|
-
*
|
|
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 //
|
|
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
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
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?:
|
|
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
|
-
})
|