@barefootjs/jsx 0.16.0 → 0.17.1
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 +73 -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/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +264 -163
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7839 -7019
- 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/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +180 -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 +714 -392
- package/src/__tests__/free-identifiers.test.ts +55 -0
- 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__/materialize-getter-calls.test.ts +58 -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 +223 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/__tests__/unsupported-expression.test.ts +98 -4
- package/src/adapters/env-signal.ts +108 -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/compiler.ts +6 -1
- package/src/expression-parser.ts +1375 -929
- package/src/index.ts +31 -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/ssr-seed-plan.ts +146 -0
- package/src/types.ts +182 -12
- package/src/__tests__/flatmap-support.test.ts +0 -218
- package/src/__tests__/reduce-op.test.ts +0 -201
|
@@ -10,41 +10,128 @@ 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
|
+
* One env signal's SSR-reader surface — the single place a future env signal
|
|
26
|
+
* registers itself so the adapter seed / memo paths stay open-closed:
|
|
27
|
+
* registering a new env signal is an analyzer factory entry + one registry
|
|
28
|
+
* entry here; adapter seed/memo paths consume the registry and need no edits.
|
|
29
|
+
*/
|
|
30
|
+
export interface EnvSignalReader {
|
|
31
|
+
/** The analyzer's `envReader` key (`'search'`). */
|
|
32
|
+
key: string
|
|
33
|
+
/**
|
|
34
|
+
* Canonical per-request reader binding every adapter's lowering
|
|
35
|
+
* canonicalises to (`searchParams` → Perl `$searchParams`, Go
|
|
36
|
+
* `in.SearchParams` via capitalisation).
|
|
37
|
+
*/
|
|
38
|
+
canonicalName: string
|
|
39
|
+
/** Reader method names the SSR lowerings recognise (`.get(key)`). */
|
|
40
|
+
methods: ReadonlySet<string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Env-signal key → its {@link EnvSignalReader} descriptor. The open-closed
|
|
45
|
+
* contract: adding a new env signal is an analyzer factory entry
|
|
46
|
+
* (`ENV_SIGNAL_FACTORIES`, #2057) + one entry here — the adapter seed / memo
|
|
47
|
+
* paths consume this registry (via {@link envSignalReaderFor} /
|
|
48
|
+
* {@link envSignalLocalNames}) and need no edits.
|
|
49
|
+
*/
|
|
50
|
+
export const ENV_SIGNAL_READERS: ReadonlyMap<string, EnvSignalReader> = new Map([
|
|
51
|
+
['search', { key: 'search', canonicalName: 'searchParams', methods: new Set(['get']) }],
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
/** Look up an env signal's reader descriptor by its `envReader` key, or `null` when unregistered/absent. */
|
|
55
|
+
export function envSignalReaderFor(key: string | undefined): EnvSignalReader | null {
|
|
56
|
+
if (key === undefined) return null
|
|
57
|
+
return ENV_SIGNAL_READERS.get(key) ?? null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The getter name(s) of env signal(s) in this component, optionally filtered
|
|
62
|
+
* to one `envReader` key.
|
|
63
|
+
*
|
|
64
|
+
* Recognised **structurally** (#2057): an env signal is declared as a
|
|
65
|
+
* `createSignal`-shaped `const [searchParams, setSearchParams] =
|
|
66
|
+
* createSearchParams()`, so the analyzer collects it into `metadata.signals`
|
|
67
|
+
* with `envReader: '<key>'` — exactly like any other signal, but tagged. This
|
|
68
|
+
* function returns those getters (whatever the destructured name is —
|
|
69
|
+
* `searchParams`, or an alias), so adapters match the reader `.get()` call
|
|
70
|
+
* against the binding actually used, with **no name allow-list** (this
|
|
71
|
+
* supersedes the import-name matching, and the closed #2055).
|
|
20
72
|
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
73
|
+
* With `key` omitted, collects every env signal's getters regardless of
|
|
74
|
+
* which reader they belong to; with `key` given, only that reader's
|
|
75
|
+
* (`searchParamsLocalNames` is the `'search'`-filtered convenience below).
|
|
76
|
+
*
|
|
77
|
+
* Empty when the component declares no matching env signal (the component
|
|
78
|
+
* keeps the generic signal lowering).
|
|
25
79
|
*/
|
|
26
|
-
export function
|
|
80
|
+
export function envSignalLocalNames(metadata: IRMetadata, key?: string): Set<string> {
|
|
27
81
|
const names = new Set<string>()
|
|
28
|
-
for (const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
32
|
-
if (s.name === 'searchParams') names.add(s.alias ?? s.name)
|
|
82
|
+
for (const s of metadata.signals) {
|
|
83
|
+
if (s.envReader !== undefined && (key === undefined || s.envReader === key)) {
|
|
84
|
+
names.add(s.getter)
|
|
33
85
|
}
|
|
34
86
|
}
|
|
35
87
|
return names
|
|
36
88
|
}
|
|
37
89
|
|
|
90
|
+
/** The getter name(s) of the `searchParams` env signal in this component. See {@link envSignalLocalNames}. */
|
|
91
|
+
export function searchParamsLocalNames(metadata: IRMetadata): Set<string> {
|
|
92
|
+
return envSignalLocalNames(metadata, 'search')
|
|
93
|
+
}
|
|
94
|
+
|
|
38
95
|
/**
|
|
39
|
-
* True when the component
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
96
|
+
* True when the component declares the `searchParams` env signal. Convenience
|
|
97
|
+
* for adapters/harnesses that only need to gate on presence (the lowering
|
|
98
|
+
* itself needs the {@link searchParamsLocalNames} set to match the actual
|
|
99
|
+
* binding in the expression).
|
|
43
100
|
*/
|
|
44
101
|
export function importsSearchParams(metadata: IRMetadata): boolean {
|
|
45
102
|
return searchParamsLocalNames(metadata).size > 0
|
|
46
103
|
}
|
|
47
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The local binding name(s) that `queryHref` is imported under in this component
|
|
107
|
+
* (#2042) — the pure URL-query builder an adapter lowers to its query helper
|
|
108
|
+
* (`bf_query` in go-template). Matched by the exported name `queryHref` and bound
|
|
109
|
+
* to `alias ?? name`, so an aliased import (`import { queryHref as qh }`) is gated
|
|
110
|
+
* against the LOCAL name. Empty when not imported.
|
|
111
|
+
*
|
|
112
|
+
* Both the main `@barefootjs/client` entry and the `@barefootjs/client/runtime`
|
|
113
|
+
* re-export are accepted: `queryHref` is exported from both, so importing it from
|
|
114
|
+
* either must enable SSR lowering — otherwise the call's object-literal arg would
|
|
115
|
+
* hit the support gate (BF101) on the runtime-entry import path.
|
|
116
|
+
*/
|
|
117
|
+
export function queryHrefLocalNames(metadata: IRMetadata): Set<string> {
|
|
118
|
+
const names = new Set<string>()
|
|
119
|
+
for (const imp of metadata.imports) {
|
|
120
|
+
if (!QUERY_HREF_SOURCES.has(imp.source) || imp.isTypeOnly) continue
|
|
121
|
+
for (const s of imp.specifiers) {
|
|
122
|
+
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
123
|
+
if (s.name === 'queryHref') names.add(s.alias ?? s.name)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return names
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Entry points that re-export `queryHref` (main + the runtime re-export). */
|
|
130
|
+
const QUERY_HREF_SOURCES: ReadonlySet<string> = new Set([
|
|
131
|
+
'@barefootjs/client',
|
|
132
|
+
'@barefootjs/client/runtime',
|
|
133
|
+
])
|
|
134
|
+
|
|
48
135
|
/**
|
|
49
136
|
* Recognise a `<binding>().<method>(<args>)` env-signal method call from a
|
|
50
137
|
* `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
|
+
}
|