@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.
- 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 +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6754 -6129
- 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/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__/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 +26 -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/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
|
@@ -216,10 +216,11 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
216
216
|
expect(bf021[0].message).toContain('not a supported shape')
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
test('
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
219
|
+
test('no BF021 for let-inline block-body sort comparator (#2040)', () => {
|
|
220
|
+
// #2040: a value-producing block body (pure `const` bindings + a terminal
|
|
221
|
+
// `return`) normalises to a single expression via let-inline, so a
|
|
222
|
+
// `{ const x = a.price; return x - b.price }` comparator now lowers exactly
|
|
223
|
+
// like the expression-bodied `(a, b) => a.price - b.price`.
|
|
223
224
|
const source = `
|
|
224
225
|
'use client'
|
|
225
226
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -239,6 +240,32 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
239
240
|
const { errors } = compileToIR(source)
|
|
240
241
|
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
241
242
|
|
|
243
|
+
expect(bf021).toHaveLength(0)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('emits BF021 error for imperative block-body sort comparator (#2040)', () => {
|
|
247
|
+
// An imperative comparator (local re-assignment / mutation) has no
|
|
248
|
+
// value-position lowering — `foldBlockToExpr` refuses it, so the arrow stays
|
|
249
|
+
// `unsupported` and the sort extraction surfaces BF021.
|
|
250
|
+
const source = `
|
|
251
|
+
'use client'
|
|
252
|
+
import { createSignal } from '@barefootjs/client'
|
|
253
|
+
|
|
254
|
+
export function TodoList() {
|
|
255
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
256
|
+
return (
|
|
257
|
+
<ul>
|
|
258
|
+
{items().sort((a, b) => { let r = 0; r = a.price - b.price; return r }).map(t => (
|
|
259
|
+
<li>{t.name}</li>
|
|
260
|
+
))}
|
|
261
|
+
</ul>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
`
|
|
265
|
+
|
|
266
|
+
const { errors } = compileToIR(source)
|
|
267
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
268
|
+
|
|
242
269
|
expect(bf021).toHaveLength(1)
|
|
243
270
|
expect(bf021[0].message).toContain('not a supported shape')
|
|
244
271
|
})
|
|
@@ -583,3 +610,70 @@ describe('Rest Pattern in Filter Predicate (BF021, #1532)', () => {
|
|
|
583
610
|
expect(bf021).toHaveLength(0)
|
|
584
611
|
})
|
|
585
612
|
})
|
|
613
|
+
|
|
614
|
+
// Block-bodied filter predicates are normalized to a single boolean expression
|
|
615
|
+
// (#2040). A value-producing block lowers like an expression predicate; an
|
|
616
|
+
// imperative block refuses.
|
|
617
|
+
describe('Block-body filter predicate normalization (#2040)', () => {
|
|
618
|
+
function loopFilterIR(predicate: string) {
|
|
619
|
+
const source = `
|
|
620
|
+
'use client'
|
|
621
|
+
import { createSignal } from '@barefootjs/client'
|
|
622
|
+
|
|
623
|
+
export function TodoList() {
|
|
624
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
625
|
+
const [filter, setFilter] = createSignal('all')
|
|
626
|
+
return (
|
|
627
|
+
<ul>
|
|
628
|
+
{items().filter(${predicate}).map(t => (
|
|
629
|
+
<li>{t.name}</li>
|
|
630
|
+
))}
|
|
631
|
+
</ul>
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
`
|
|
635
|
+
const { ir, errors } = compileToIR(source)
|
|
636
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
637
|
+
// Find the loop node carrying the filterPredicate.
|
|
638
|
+
let found: any = null
|
|
639
|
+
const walk = (n: any) => {
|
|
640
|
+
if (!n || found) return
|
|
641
|
+
if (n.filterPredicate) found = n
|
|
642
|
+
for (const c of n.children ?? []) walk(c)
|
|
643
|
+
}
|
|
644
|
+
walk(ir)
|
|
645
|
+
return { bf021, filterPredicate: found?.filterPredicate }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
test('value-producing block (let-inline + early return) folds to a predicate', () => {
|
|
649
|
+
const { bf021, filterPredicate } = loopFilterIR(`t => {
|
|
650
|
+
const f = filter()
|
|
651
|
+
if (f === 'active') return !t.done
|
|
652
|
+
if (f === 'completed') return t.done
|
|
653
|
+
return true
|
|
654
|
+
}`)
|
|
655
|
+
expect(bf021).toHaveLength(0)
|
|
656
|
+
// No leftover block shape — a single boolean predicate expression.
|
|
657
|
+
expect(filterPredicate?.predicate).toBeDefined()
|
|
658
|
+
expect((filterPredicate as any)?.blockBody).toBeUndefined()
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
test('signal read on multiple branches still folds (idempotent getter is pure)', () => {
|
|
662
|
+
const { bf021, filterPredicate } = loopFilterIR(`t => {
|
|
663
|
+
const f = filter()
|
|
664
|
+
if (f === 'active') return !t.done
|
|
665
|
+
return f === 'completed' ? t.done : true
|
|
666
|
+
}`)
|
|
667
|
+
expect(bf021).toHaveLength(0)
|
|
668
|
+
expect(filterPredicate?.predicate).toBeDefined()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
test('imperative block (local re-assignment) refuses with BF021', () => {
|
|
672
|
+
const { bf021 } = loopFilterIR(`t => {
|
|
673
|
+
let keep = false
|
|
674
|
+
keep = !t.done
|
|
675
|
+
return keep
|
|
676
|
+
}`)
|
|
677
|
+
expect(bf021.length).toBeGreaterThan(0)
|
|
678
|
+
})
|
|
679
|
+
})
|
|
@@ -10,41 +10,80 @@ import type { ParsedExpr } from '../expression-parser.ts'
|
|
|
10
10
|
import type { IRMetadata } from '../types.ts'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
13
|
+
* Env-signal key → the runtime factory that produces it (`'search'` →
|
|
14
|
+
* `createSearchParams`). Single source of truth for the reverse of the
|
|
15
|
+
* analyzer's `ENV_SIGNAL_FACTORIES` (#2057): an env signal is `createSignal`-
|
|
16
|
+
* shaped for analysis, but every backend that re-emits its declaration (client
|
|
17
|
+
* JS, JSX/Hono SSR) must call this factory, not `createSignal`, so the value is
|
|
18
|
+
* the request-scoped reader rather than stored state.
|
|
19
|
+
*/
|
|
20
|
+
export const ENV_SIGNAL_CLIENT_FACTORY: Record<string, string> = {
|
|
21
|
+
search: 'createSearchParams',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The getter name(s) of the `searchParams` env signal in this component.
|
|
26
|
+
*
|
|
27
|
+
* Recognised **structurally** (#2057): the env signal is now declared as a
|
|
28
|
+
* `createSignal`-shaped `const [searchParams, setSearchParams] =
|
|
29
|
+
* createSearchParams()`, so the analyzer collects it into `metadata.signals`
|
|
30
|
+
* with `envReader: 'search'` — exactly like any other signal, but tagged. This
|
|
31
|
+
* function returns those getters (whatever the destructured name is —
|
|
32
|
+
* `searchParams`, or an alias), so adapters match the reader `.get()` call
|
|
33
|
+
* against the binding actually used, with **no `searchParams`-name allow-list**
|
|
34
|
+
* (this supersedes the import-name matching, and the closed #2055).
|
|
20
35
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 'searchParams'` and the local binding is `alias ?? name`. Namespace / default
|
|
24
|
-
* specifiers bind a different identifier and are excluded.
|
|
36
|
+
* Empty when the component declares no env signal (the component keeps the
|
|
37
|
+
* generic signal lowering).
|
|
25
38
|
*/
|
|
26
39
|
export function searchParamsLocalNames(metadata: IRMetadata): Set<string> {
|
|
27
40
|
const names = new Set<string>()
|
|
28
|
-
for (const
|
|
29
|
-
if (
|
|
30
|
-
for (const s of imp.specifiers) {
|
|
31
|
-
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
32
|
-
if (s.name === 'searchParams') names.add(s.alias ?? s.name)
|
|
33
|
-
}
|
|
41
|
+
for (const s of metadata.signals) {
|
|
42
|
+
if (s.envReader === 'search') names.add(s.getter)
|
|
34
43
|
}
|
|
35
44
|
return names
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
* True when the component
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
48
|
+
* True when the component declares the `searchParams` env signal. Convenience
|
|
49
|
+
* for adapters/harnesses that only need to gate on presence (the lowering
|
|
50
|
+
* itself needs the {@link searchParamsLocalNames} set to match the actual
|
|
51
|
+
* binding in the expression).
|
|
43
52
|
*/
|
|
44
53
|
export function importsSearchParams(metadata: IRMetadata): boolean {
|
|
45
54
|
return searchParamsLocalNames(metadata).size > 0
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The local binding name(s) that `queryHref` is imported under in this component
|
|
59
|
+
* (#2042) — the pure URL-query builder an adapter lowers to its query helper
|
|
60
|
+
* (`bf_query` in go-template). Matched by the exported name `queryHref` and bound
|
|
61
|
+
* to `alias ?? name`, so an aliased import (`import { queryHref as qh }`) is gated
|
|
62
|
+
* against the LOCAL name. Empty when not imported.
|
|
63
|
+
*
|
|
64
|
+
* Both the main `@barefootjs/client` entry and the `@barefootjs/client/runtime`
|
|
65
|
+
* re-export are accepted: `queryHref` is exported from both, so importing it from
|
|
66
|
+
* either must enable SSR lowering — otherwise the call's object-literal arg would
|
|
67
|
+
* hit the support gate (BF101) on the runtime-entry import path.
|
|
68
|
+
*/
|
|
69
|
+
export function queryHrefLocalNames(metadata: IRMetadata): Set<string> {
|
|
70
|
+
const names = new Set<string>()
|
|
71
|
+
for (const imp of metadata.imports) {
|
|
72
|
+
if (!QUERY_HREF_SOURCES.has(imp.source) || imp.isTypeOnly) continue
|
|
73
|
+
for (const s of imp.specifiers) {
|
|
74
|
+
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
75
|
+
if (s.name === 'queryHref') names.add(s.alias ?? s.name)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return names
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Entry points that re-export `queryHref` (main + the runtime re-export). */
|
|
82
|
+
const QUERY_HREF_SOURCES: ReadonlySet<string> = new Set([
|
|
83
|
+
'@barefootjs/client',
|
|
84
|
+
'@barefootjs/client/runtime',
|
|
85
|
+
])
|
|
86
|
+
|
|
48
87
|
/**
|
|
49
88
|
* Recognise a `<binding>().<method>(<args>)` env-signal method call from a
|
|
50
89
|
* `call` node's callee + args, where `<binding>` is one of the local names
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from '../types.ts'
|
|
13
13
|
import { BF_SCOPE, BF_SLOT, BF_COND } from '@barefootjs/shared'
|
|
14
14
|
import { BaseAdapter } from './interface.ts'
|
|
15
|
+
import { ENV_SIGNAL_CLIENT_FACTORY } from './env-signal.ts'
|
|
15
16
|
import { formatParamWithType, findReachableNames } from '../module-exports.ts'
|
|
16
17
|
|
|
17
18
|
export interface JsxAdapterConfig {
|
|
@@ -110,6 +111,22 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
110
111
|
|
|
111
112
|
for (const signal of ir.metadata.signals) {
|
|
112
113
|
if (signal.isModule) continue
|
|
114
|
+
if (signal.envReader) {
|
|
115
|
+
// Env signal (#2057): call the real runtime factory so SSR resolves the
|
|
116
|
+
// request query through the installed server env reader, not a static
|
|
117
|
+
// initial value. Emit the factory as written (alias / namespace aware),
|
|
118
|
+
// matching the import re-emitted into the SSR module; fall back to the
|
|
119
|
+
// canonical name if the callee text wasn't captured.
|
|
120
|
+
const factory = signal.envFactory ?? ENV_SIGNAL_CLIENT_FACTORY[signal.envReader]
|
|
121
|
+
if (factory) {
|
|
122
|
+
lines.push(
|
|
123
|
+
signal.setter
|
|
124
|
+
? ` const [${signal.getter}, ${signal.setter}] = ${factory}()`
|
|
125
|
+
: ` const [${signal.getter}] = ${factory}()`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
113
130
|
// Create a getter that returns the initial value for SSR
|
|
114
131
|
const rawInitialValue = preserveTypes
|
|
115
132
|
? (signal.typedInitialValue ?? signal.initialValue)
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
* be added in one place.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import type { ParsedExpr,
|
|
36
|
+
import type { ParsedExpr, FlatDepth, TemplatePart, ObjectLiteralProperty } from '../expression-parser.ts'
|
|
37
|
+
import { asCallbackMethodCall } from '../expression-parser.ts'
|
|
37
38
|
|
|
38
39
|
export type HigherOrderMethod = 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex'
|
|
39
40
|
|
|
@@ -139,49 +140,48 @@ export interface ParsedExprEmitter {
|
|
|
139
140
|
emit: (e: ParsedExpr) => string,
|
|
140
141
|
): string
|
|
141
142
|
templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// A higher-order callback method call (`<object>.<method>(<arrow>, …rest)`,
|
|
144
|
+
// method ∈ `CALLBACK_METHODS`): `.filter`/`.find`/`.every`/`.some`/`.sort`/
|
|
145
|
+
// `.reduce`/`.flatMap`/… (#2018 P5). The adapter serializes the `arrow` body
|
|
146
|
+
// to the runtime evaluator (eval-first) and falls back to a structured
|
|
147
|
+
// lowering when the body is outside the evaluator surface. `restArgs` carries
|
|
148
|
+
// any trailing arguments (e.g. the `.reduce` init).
|
|
149
|
+
callbackMethod(
|
|
150
|
+
method: string,
|
|
145
151
|
object: ParsedExpr,
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
|
|
153
|
+
restArgs: ParsedExpr[],
|
|
148
154
|
emit: (e: ParsedExpr) => string,
|
|
149
155
|
): string
|
|
156
|
+
// A standalone arrow / regex literal. These normally reach an adapter only as
|
|
157
|
+
// a callback argument (handled by `callbackMethod`); emitted standalone they
|
|
158
|
+
// have no template form, so adapters route them to their `unsupported` path.
|
|
159
|
+
arrow(params: string[], body: ParsedExpr, emit: (e: ParsedExpr) => string): string
|
|
160
|
+
regex(raw: string): string
|
|
150
161
|
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string
|
|
162
|
+
// Emit an object literal `{ a: 1, b: x }`. `raw` is the original
|
|
163
|
+
// expression string so an adapter that doesn't lower object values yet
|
|
164
|
+
// can delegate to `unsupported(raw, …)` and stay byte-identical.
|
|
165
|
+
objectLiteral(
|
|
166
|
+
properties: ObjectLiteralProperty[],
|
|
167
|
+
raw: string,
|
|
168
|
+
emit: (e: ParsedExpr) => string,
|
|
169
|
+
): string
|
|
151
170
|
arrayMethod(
|
|
152
171
|
method: ArrayMethod,
|
|
153
172
|
object: ParsedExpr,
|
|
154
173
|
args: ParsedExpr[],
|
|
155
174
|
emit: (e: ParsedExpr) => string,
|
|
156
175
|
): string
|
|
157
|
-
sortMethod(
|
|
158
|
-
method: SortMethod,
|
|
159
|
-
object: ParsedExpr,
|
|
160
|
-
comparator: SortComparator,
|
|
161
|
-
emit: (e: ParsedExpr) => string,
|
|
162
|
-
): string
|
|
163
|
-
reduceMethod(
|
|
164
|
-
method: ReduceMethod,
|
|
165
|
-
object: ParsedExpr,
|
|
166
|
-
reduceOp: ReduceOp,
|
|
167
|
-
emit: (e: ParsedExpr) => string,
|
|
168
|
-
): string
|
|
169
176
|
// `.flat(depth?)` gets its own dispatcher arm (#1448 Tier C): it carries
|
|
170
177
|
// a structured `FlatDepth` (the validated literal / `'infinity'`) rather
|
|
171
|
-
// than a `ParsedExpr[]` args list,
|
|
178
|
+
// than a `ParsedExpr[]` args list. Non-callback, so it is NOT routed
|
|
179
|
+
// through `callbackMethod`.
|
|
172
180
|
flatMethod(
|
|
173
181
|
object: ParsedExpr,
|
|
174
182
|
depth: FlatDepth,
|
|
175
183
|
emit: (e: ParsedExpr) => string,
|
|
176
184
|
): string
|
|
177
|
-
// `.flatMap(fn)` value-returning field projection gets its own arm
|
|
178
|
-
// (#1448 Tier C): it carries a structured `FlatMapOp` rather than a
|
|
179
|
-
// `ParsedExpr[]` args list, same rationale as sort / reduce / flat.
|
|
180
|
-
flatMapMethod(
|
|
181
|
-
object: ParsedExpr,
|
|
182
|
-
op: FlatMapOp,
|
|
183
|
-
emit: (e: ParsedExpr) => string,
|
|
184
|
-
): string
|
|
185
185
|
unsupported(raw: string, reason: string): string
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -201,8 +201,13 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
201
201
|
return emitter.identifier(expr.name)
|
|
202
202
|
case 'literal':
|
|
203
203
|
return emitter.literal(expr.value, expr.literalType)
|
|
204
|
-
case 'call':
|
|
204
|
+
case 'call': {
|
|
205
|
+
// A higher-order callback call (`arr.filter(p)` / `arr.sort(cmp)` / …)
|
|
206
|
+
// routes to the dedicated `callbackMethod` arm; any other call is generic.
|
|
207
|
+
const cb = asCallbackMethodCall(expr)
|
|
208
|
+
if (cb) return emitter.callbackMethod(cb.method, cb.object, cb.arrow, cb.args, emit)
|
|
205
209
|
return emitter.call(expr.callee, expr.args, emit)
|
|
210
|
+
}
|
|
206
211
|
case 'member':
|
|
207
212
|
return emitter.member(expr.object, expr.property, expr.computed, emit)
|
|
208
213
|
case 'index-access':
|
|
@@ -217,25 +222,18 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
217
222
|
return emitter.conditional(expr.test, expr.consequent, expr.alternate, emit)
|
|
218
223
|
case 'template-literal':
|
|
219
224
|
return emitter.templateLiteral(expr.parts, emit)
|
|
220
|
-
case 'arrow
|
|
221
|
-
return emitter.
|
|
222
|
-
case '
|
|
223
|
-
return emitter.
|
|
225
|
+
case 'arrow':
|
|
226
|
+
return emitter.arrow(expr.params, expr.body, emit)
|
|
227
|
+
case 'regex':
|
|
228
|
+
return emitter.regex(expr.raw)
|
|
224
229
|
case 'array-literal':
|
|
225
230
|
return emitter.arrayLiteral(expr.elements, emit)
|
|
231
|
+
case 'object-literal':
|
|
232
|
+
return emitter.objectLiteral(expr.properties, expr.raw, emit)
|
|
226
233
|
case 'array-method':
|
|
227
|
-
if (expr.method === 'sort' || expr.method === 'toSorted') {
|
|
228
|
-
return emitter.sortMethod(expr.method, expr.object, expr.comparator, emit)
|
|
229
|
-
}
|
|
230
|
-
if (expr.method === 'reduce' || expr.method === 'reduceRight') {
|
|
231
|
-
return emitter.reduceMethod(expr.method, expr.object, expr.reduceOp, emit)
|
|
232
|
-
}
|
|
233
234
|
if (expr.method === 'flat') {
|
|
234
235
|
return emitter.flatMethod(expr.object, expr.flatDepth, emit)
|
|
235
236
|
}
|
|
236
|
-
if (expr.method === 'flatMap') {
|
|
237
|
-
return emitter.flatMapMethod(expr.object, expr.flatMapOp, emit)
|
|
238
|
-
}
|
|
239
237
|
return emitter.arrayMethod(expr.method, expr.object, expr.args, emit)
|
|
240
238
|
case 'unsupported':
|
|
241
239
|
return emitter.unsupported(expr.raw, expr.reason)
|
package/src/analyzer-context.ts
CHANGED
|
@@ -344,11 +344,21 @@ function propertyNameText(
|
|
|
344
344
|
|
|
345
345
|
export function typeNodeToTypeInfo(
|
|
346
346
|
typeNode: ts.TypeNode | undefined,
|
|
347
|
-
sourceFile: ts.SourceFile
|
|
347
|
+
sourceFile: ts.SourceFile,
|
|
348
|
+
// When set, `raw` is derived from this extractor instead of
|
|
349
|
+
// `node.getText(sourceFile)`. Synthetic nodes (from `checker.typeToTypeNode`,
|
|
350
|
+
// used by `tsTypeToTypeInfo`) have no source positions, so `getText()` would
|
|
351
|
+
// throw — callers pass a printer-based extractor. In that mode the
|
|
352
|
+
// object-literal / function branches that need member/param source text emit
|
|
353
|
+
// a `raw`-only shape (those consumers only run on real source).
|
|
354
|
+
rawOf?: (node: ts.TypeNode) => string
|
|
348
355
|
): TypeInfo | null {
|
|
349
356
|
if (!typeNode) return null
|
|
350
357
|
|
|
351
|
-
const
|
|
358
|
+
const synthetic = rawOf !== undefined
|
|
359
|
+
const raw = rawOf ? rawOf(typeNode) : typeNode.getText(sourceFile)
|
|
360
|
+
const recurse = (n: ts.TypeNode): TypeInfo =>
|
|
361
|
+
typeNodeToTypeInfo(n, sourceFile, rawOf) ?? { kind: 'unknown', raw: 'unknown' }
|
|
352
362
|
|
|
353
363
|
// Primitive types (check by SyntaxKind)
|
|
354
364
|
switch (typeNode.kind) {
|
|
@@ -366,26 +376,12 @@ export function typeNodeToTypeInfo(
|
|
|
366
376
|
|
|
367
377
|
// Array types
|
|
368
378
|
if (ts.isArrayTypeNode(typeNode)) {
|
|
369
|
-
return {
|
|
370
|
-
kind: 'array',
|
|
371
|
-
raw,
|
|
372
|
-
elementType: typeNodeToTypeInfo(typeNode.elementType, sourceFile) ?? {
|
|
373
|
-
kind: 'unknown',
|
|
374
|
-
raw: 'unknown',
|
|
375
|
-
},
|
|
376
|
-
}
|
|
379
|
+
return { kind: 'array', raw, elementType: recurse(typeNode.elementType) }
|
|
377
380
|
}
|
|
378
381
|
|
|
379
382
|
// Union types
|
|
380
383
|
if (ts.isUnionTypeNode(typeNode)) {
|
|
381
|
-
return {
|
|
382
|
-
kind: 'union',
|
|
383
|
-
raw,
|
|
384
|
-
unionTypes: typeNode.types.map(
|
|
385
|
-
(t) =>
|
|
386
|
-
typeNodeToTypeInfo(t, sourceFile) ?? { kind: 'unknown', raw: 'unknown' }
|
|
387
|
-
),
|
|
388
|
-
}
|
|
384
|
+
return { kind: 'union', raw, unionTypes: typeNode.types.map(recurse) }
|
|
389
385
|
}
|
|
390
386
|
|
|
391
387
|
// Type literal (object type)
|
|
@@ -393,7 +389,9 @@ export function typeNodeToTypeInfo(
|
|
|
393
389
|
return {
|
|
394
390
|
kind: 'object',
|
|
395
391
|
raw,
|
|
396
|
-
|
|
392
|
+
// Synthetic members have no source positions for membersToProperties'
|
|
393
|
+
// getText(); emit a property-less object in that mode.
|
|
394
|
+
...(synthetic ? {} : { properties: membersToProperties(typeNode.members, sourceFile) }),
|
|
397
395
|
}
|
|
398
396
|
}
|
|
399
397
|
|
|
@@ -407,14 +405,7 @@ export function typeNodeToTypeInfo(
|
|
|
407
405
|
(refName === 'Array' || refName === 'ReadonlyArray') &&
|
|
408
406
|
typeNode.typeArguments?.length === 1
|
|
409
407
|
) {
|
|
410
|
-
return {
|
|
411
|
-
kind: 'array',
|
|
412
|
-
raw,
|
|
413
|
-
elementType: typeNodeToTypeInfo(typeNode.typeArguments[0], sourceFile) ?? {
|
|
414
|
-
kind: 'unknown',
|
|
415
|
-
raw: 'unknown',
|
|
416
|
-
},
|
|
417
|
-
}
|
|
408
|
+
return { kind: 'array', raw, elementType: recurse(typeNode.typeArguments[0]) }
|
|
418
409
|
}
|
|
419
410
|
return {
|
|
420
411
|
kind: 'interface',
|
|
@@ -424,6 +415,8 @@ export function typeNodeToTypeInfo(
|
|
|
424
415
|
|
|
425
416
|
// Function type
|
|
426
417
|
if (ts.isFunctionTypeNode(typeNode)) {
|
|
418
|
+
// Param names/defaults come from getText — skip them for synthetic nodes.
|
|
419
|
+
if (synthetic) return { kind: 'function', raw }
|
|
427
420
|
return {
|
|
428
421
|
kind: 'function',
|
|
429
422
|
raw,
|
|
@@ -443,6 +436,40 @@ export function typeNodeToTypeInfo(
|
|
|
443
436
|
return { kind: 'unknown', raw }
|
|
444
437
|
}
|
|
445
438
|
|
|
439
|
+
// Printer + blank source file reused across calls so checker-driven type
|
|
440
|
+
// conversion never allocates a fresh `ts.createSourceFile` per memo on the
|
|
441
|
+
// build hot path (the blank file is only printer context for synthetic
|
|
442
|
+
// nodes, created once at module load).
|
|
443
|
+
const _typePrinter = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true })
|
|
444
|
+
const _blankTypeSourceFile = ts.createSourceFile('__bf_types__.ts', '', ts.ScriptTarget.Latest)
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Convert a resolved `ts.Type` to `TypeInfo` via the type checker. Used to
|
|
448
|
+
* sharpen `createMemo` field types the syntactic `inferTypeFromValue`
|
|
449
|
+
* heuristic can't reach — e.g. `createMemo(() => generateDays())` whose body
|
|
450
|
+
* is a local-function call (→ `CalendarDay[][]`) or a ternary of typed
|
|
451
|
+
* arrays (→ `string[]`). Without this the Go adapter renders such memos as
|
|
452
|
+
* `map[string]interface{}` / `bool` placeholders, so a typed backend can't
|
|
453
|
+
* populate the SSR data (#1968). Returns `null` when the type can't be
|
|
454
|
+
* lowered to a `ts.TypeNode`.
|
|
455
|
+
*
|
|
456
|
+
* Shares the structural lowering in `typeNodeToTypeInfo` (via the `rawOf`
|
|
457
|
+
* extractor) so node→TypeInfo logic lives in one place; the synthetic nodes
|
|
458
|
+
* from `typeToTypeNode` have no source text, so `raw` comes from the printer.
|
|
459
|
+
*/
|
|
460
|
+
export function tsTypeToTypeInfo(type: ts.Type, checker: ts.TypeChecker): TypeInfo | null {
|
|
461
|
+
const node = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation)
|
|
462
|
+
if (!node) return null
|
|
463
|
+
const rawOf = (n: ts.TypeNode): string => {
|
|
464
|
+
try {
|
|
465
|
+
return _typePrinter.printNode(ts.EmitHint.Unspecified, n, _blankTypeSourceFile)
|
|
466
|
+
} catch {
|
|
467
|
+
return 'unknown'
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return typeNodeToTypeInfo(node, _blankTypeSourceFile, rawOf)
|
|
471
|
+
}
|
|
472
|
+
|
|
446
473
|
// =============================================================================
|
|
447
474
|
// AST Helpers
|
|
448
475
|
// =============================================================================
|
|
@@ -474,3 +501,21 @@ export function isArrowComponentFunction(
|
|
|
474
501
|
if (!node.initializer || !ts.isArrowFunction(node.initializer)) return false
|
|
475
502
|
return true
|
|
476
503
|
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The set of reactive getter names for a component — signal accessors plus memo
|
|
507
|
+
* names. Single source of truth for "what counts as a reactive getter", shared
|
|
508
|
+
* by the analyzer's block-memo fold purity oracle (#2040) and `jsx-to-ir`'s
|
|
509
|
+
* `getReactiveGetterNames`. A reactive read is idempotent within a render, so
|
|
510
|
+
* callers treat `getter()` as pure. If a third reactive kind is added, update
|
|
511
|
+
* this one function.
|
|
512
|
+
*/
|
|
513
|
+
export function collectReactiveGetterNames(
|
|
514
|
+
signals: ReadonlyArray<{ getter: string }>,
|
|
515
|
+
memos: ReadonlyArray<{ name: string }>,
|
|
516
|
+
): Set<string> {
|
|
517
|
+
const names = new Set<string>()
|
|
518
|
+
for (const s of signals) names.add(s.getter)
|
|
519
|
+
for (const m of memos) names.add(m.name)
|
|
520
|
+
return names
|
|
521
|
+
}
|