@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
|
@@ -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
|
+
}
|
package/src/ssr-defaults.ts
CHANGED
|
@@ -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) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-neutral SSR seed plan — which signals/memos an adapter may seed
|
|
3
|
+
* in-template at SSR time, and from what scope.
|
|
4
|
+
*
|
|
5
|
+
* Design principle: the IR/analyzer side ANALYZES and attaches structured
|
|
6
|
+
* information; adapters only EMIT. The "is this binding derivable from names
|
|
7
|
+
* already in template scope" decision used to live (triplicated) in the
|
|
8
|
+
* template adapters' seed paths; this module computes it once, on the IR, so
|
|
9
|
+
* every adapter consumes the same plan and only supplies its own syntax.
|
|
10
|
+
*
|
|
11
|
+
* Ordering / acyclicity guarantee: `steps` lists the component's signals
|
|
12
|
+
* first, then its memos, each group in declaration order (matching
|
|
13
|
+
* `IRMetadata.signals` / `IRMetadata.memos`). A binding's name only enters
|
|
14
|
+
* the scope set AFTER its own step is decided, so a `derived` step's `frees`
|
|
15
|
+
* can only name `baseScope` entries or EARLIER steps — self- and
|
|
16
|
+
* forward-references are rejected by construction, and a consumer emitting
|
|
17
|
+
* the steps top-to-bottom never reads an undeclared local.
|
|
18
|
+
*
|
|
19
|
+
* Module-scope pure-string consts count as in-scope (they are part of
|
|
20
|
+
* `baseScope`) because every adapter compile-time-inlines them to their
|
|
21
|
+
* literal value — `collectModuleStringConsts` is the shared source of that
|
|
22
|
+
* set — so a reference to one is never a template-variable read.
|
|
23
|
+
*
|
|
24
|
+
* A `derived` step with EMPTY `frees` is a constant expression (e.g.
|
|
25
|
+
* `createSignal('b')`): the plan still classifies it as derived because the
|
|
26
|
+
* expression is fully analyzable; emit-side constant-skipping (adapters keep
|
|
27
|
+
* their existing ssr-defaults seeding for such inits) is an adapter concern,
|
|
28
|
+
* not a plan concern. Likewise the plan makes no backend-specific choices —
|
|
29
|
+
* no target-variable checks, no self-shadowing rules, no per-backend shape
|
|
30
|
+
* catalogs — those stay in the adapters.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { collectModuleStringConsts } from './augment-inherited-props.ts'
|
|
34
|
+
import { envSignalReaderFor, type EnvSignalReader } from './adapters/env-signal.ts'
|
|
35
|
+
import {
|
|
36
|
+
extractArrowBodyExpression,
|
|
37
|
+
freeIdentifiers,
|
|
38
|
+
isSupported,
|
|
39
|
+
parseExpression,
|
|
40
|
+
type ParsedExpr,
|
|
41
|
+
} from './expression-parser.ts'
|
|
42
|
+
import type { IRMetadata } from './types.ts'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One binding in component declaration order (signals first, then memos —
|
|
46
|
+
* matching `IRMetadata` order and the adapters' iteration).
|
|
47
|
+
*
|
|
48
|
+
* - `env-reader`: an env signal whose `envReader` key resolves in the shared
|
|
49
|
+
* registry (`envSignalReaderFor`). The runtime provides the per-request
|
|
50
|
+
* reader, so there is nothing to seed; the name still enters scope so a
|
|
51
|
+
* later derived step may reference it. An `envReader` key UNKNOWN to the
|
|
52
|
+
* registry falls through to the normal derived/opaque rules instead.
|
|
53
|
+
* - `derived`: the binding's value expression is a supported shape whose free
|
|
54
|
+
* identifiers are all in scope at this point (baseScope + earlier steps) —
|
|
55
|
+
* an adapter may seed it in-template by lowering `parsed`/`expr`.
|
|
56
|
+
* - `opaque`: not seedable this way (empty init, unsupported shape,
|
|
57
|
+
* unanalyzable free set, out-of-scope reference, or a block-bodied memo).
|
|
58
|
+
* The name still enters scope for later steps; adapters keep their static
|
|
59
|
+
* ssr-defaults seeding for it.
|
|
60
|
+
*/
|
|
61
|
+
export type SsrSeedStep =
|
|
62
|
+
| { kind: 'env-reader'; name: string; reader: EnvSignalReader }
|
|
63
|
+
| { kind: 'derived'; name: string; origin: 'signal' | 'memo'; expr: string; parsed: ParsedExpr; frees: string[] }
|
|
64
|
+
| { kind: 'opaque'; name: string; origin: 'signal' | 'memo' }
|
|
65
|
+
|
|
66
|
+
export interface SsrSeedPlan {
|
|
67
|
+
/**
|
|
68
|
+
* Names in scope before any step: props params, the props-object name
|
|
69
|
+
* (when the component takes an undestructured props object), and module
|
|
70
|
+
* pure-string consts (compile-time inlined by every adapter).
|
|
71
|
+
*/
|
|
72
|
+
baseScope: string[]
|
|
73
|
+
steps: SsrSeedStep[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Classify one value expression against the current scope: `derived` when it
|
|
78
|
+
* parses to a supported shape whose free identifiers are all `available`
|
|
79
|
+
* (an unanalyzable free set — `freeIdentifiers` → null — fails safe to
|
|
80
|
+
* opaque). The scope check runs over the parsed SOURCE tree, so a shadowed
|
|
81
|
+
* name (`items.filter((p) => p.ok) && p`, where the trailing `p` is a
|
|
82
|
+
* different, unbound reference from the callback's own param) is rejected.
|
|
83
|
+
*/
|
|
84
|
+
function classify(
|
|
85
|
+
name: string,
|
|
86
|
+
origin: 'signal' | 'memo',
|
|
87
|
+
expr: string,
|
|
88
|
+
parsed: ParsedExpr,
|
|
89
|
+
available: ReadonlySet<string>,
|
|
90
|
+
): SsrSeedStep {
|
|
91
|
+
if (!isSupported(parsed).supported) return { kind: 'opaque', name, origin }
|
|
92
|
+
const frees = freeIdentifiers(parsed)
|
|
93
|
+
if (frees === null) return { kind: 'opaque', name, origin }
|
|
94
|
+
for (const free of frees) {
|
|
95
|
+
if (!available.has(free)) return { kind: 'opaque', name, origin }
|
|
96
|
+
}
|
|
97
|
+
return { kind: 'derived', name, origin, expr, parsed, frees: [...frees] }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Compute the component's SSR seed plan from its metadata. See the module
|
|
102
|
+
* doc for the contract. Memo steps are gated to EXPRESSION-BODIED memos
|
|
103
|
+
* (`extractArrowBodyExpression` returns the body): a block-bodied memo is
|
|
104
|
+
* `opaque` even when the analyzer folded it to a `parsed` expression.
|
|
105
|
+
*/
|
|
106
|
+
export function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan {
|
|
107
|
+
const baseScope: string[] = metadata.propsParams.map(p => p.name)
|
|
108
|
+
if (metadata.propsObjectName) baseScope.push(metadata.propsObjectName)
|
|
109
|
+
for (const name of collectModuleStringConsts(metadata.localConstants).keys()) {
|
|
110
|
+
baseScope.push(name)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const available = new Set<string>(baseScope)
|
|
114
|
+
const steps: SsrSeedStep[] = []
|
|
115
|
+
|
|
116
|
+
for (const signal of metadata.signals) {
|
|
117
|
+
if (signal.envReader) {
|
|
118
|
+
const reader = envSignalReaderFor(signal.envReader)
|
|
119
|
+
if (reader) {
|
|
120
|
+
steps.push({ kind: 'env-reader', name: signal.getter, reader })
|
|
121
|
+
available.add(signal.getter)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const expr = signal.initialValue.trim()
|
|
126
|
+
steps.push(
|
|
127
|
+
expr === ''
|
|
128
|
+
? { kind: 'opaque', name: signal.getter, origin: 'signal' }
|
|
129
|
+
: classify(signal.getter, 'signal', expr, parseExpression(expr), available),
|
|
130
|
+
)
|
|
131
|
+
available.add(signal.getter)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const memo of metadata.memos) {
|
|
135
|
+
const body = extractArrowBodyExpression(memo.computation)
|
|
136
|
+
const expr = body?.trim() ?? ''
|
|
137
|
+
steps.push(
|
|
138
|
+
expr === ''
|
|
139
|
+
? { kind: 'opaque', name: memo.name, origin: 'memo' }
|
|
140
|
+
: classify(memo.name, 'memo', expr, memo.parsed ?? parseExpression(expr), available),
|
|
141
|
+
)
|
|
142
|
+
available.add(memo.name)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { baseScope, steps }
|
|
146
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,26 @@
|
|
|
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
|
+
import type { SsrSeedPlan } from './ssr-seed-plan.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loop-hoisted sort comparator for the `.sort().map()` / `.toSorted().map()`
|
|
12
|
+
* pattern (#2018 P5). Carries the generic comparator `arrow` (params + body)
|
|
13
|
+
* that the SSR adapter serializes to the runtime evaluator (eval-first) or, for
|
|
14
|
+
* a `localeCompare` comparator the evaluator can't model, recovers a structured
|
|
15
|
+
* comparator from via `sortComparatorFromArrow`. The `paramA` / `paramB` / `raw`
|
|
16
|
+
* fields round-trip the comparator to native JS for the client / CSR path
|
|
17
|
+
* (`(paramA, paramB) => raw`), so the client is untouched.
|
|
18
|
+
*/
|
|
19
|
+
export type IRLoopSort = {
|
|
20
|
+
// Always the comparator arrow itself — narrowed so consumers read
|
|
21
|
+
// `.params` / `.body` without a defensive `kind` check or non-null assertion.
|
|
22
|
+
arrow: Extract<ParsedExpr, { kind: 'arrow' }>
|
|
23
|
+
paramA: string
|
|
24
|
+
paramB: string
|
|
25
|
+
raw: string
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
// =============================================================================
|
|
10
29
|
// Source Location (for Error Reporting)
|
|
@@ -323,6 +342,20 @@ export interface IRExpression {
|
|
|
323
342
|
expr: string
|
|
324
343
|
/** Pre-transformed expr with destructured prop refs rewritten to _p.xxx (for client JS templates). */
|
|
325
344
|
templateExpr?: string
|
|
345
|
+
/**
|
|
346
|
+
* Structured parse of `expr` (`parseExpression(expr.trim())`), attached once
|
|
347
|
+
* during IR construction so SSR adapters emit from the tree instead of each
|
|
348
|
+
* re-parsing the string at emit time (and so a multi-adapter build parses it
|
|
349
|
+
* once, not per adapter). Plain serializable data.
|
|
350
|
+
*
|
|
351
|
+
* OPTIONAL by design — consumers MUST fall back to parsing `expr` when it is
|
|
352
|
+
* missing. It is absent for an empty/whitespace `expr`, and may also be
|
|
353
|
+
* absent for a node the IR-build walk doesn't reach (the walk is best-effort;
|
|
354
|
+
* under-coverage is a missed optimization, never a behavioural change). When
|
|
355
|
+
* present, an unparsable expression is a `{ kind: 'unsupported' }` node (the
|
|
356
|
+
* adapter's own support gate handles it).
|
|
357
|
+
*/
|
|
358
|
+
parsed?: ParsedExpr
|
|
326
359
|
typeInfo: TypeInfo | null
|
|
327
360
|
reactive: boolean
|
|
328
361
|
slotId: string | null
|
|
@@ -347,6 +380,13 @@ export interface IRConditional {
|
|
|
347
380
|
condition: string
|
|
348
381
|
/** Pre-transformed condition with destructured prop refs rewritten to _p.xxx. */
|
|
349
382
|
templateCondition?: string
|
|
383
|
+
/**
|
|
384
|
+
* Structured parse of `condition` (`parseExpression(condition.trim())`),
|
|
385
|
+
* attached during IR construction so adapters lower the condition from the
|
|
386
|
+
* tree instead of re-parsing the string. Optional/best-effort — see
|
|
387
|
+
* `IRExpression.parsed`; consumers fall back to parsing `condition`.
|
|
388
|
+
*/
|
|
389
|
+
parsedCondition?: ParsedExpr
|
|
350
390
|
conditionType: TypeInfo | null
|
|
351
391
|
reactive: boolean
|
|
352
392
|
whenTrue: IRNode
|
|
@@ -404,6 +444,14 @@ export interface IRLoop {
|
|
|
404
444
|
*/
|
|
405
445
|
method?: 'flatMap'
|
|
406
446
|
array: string
|
|
447
|
+
/**
|
|
448
|
+
* Structured parse of `array` (`parseExpression(array.trim())`), attached
|
|
449
|
+
* during IR construction so adapters lower the loop's array from the tree
|
|
450
|
+
* instead of re-parsing the string (e.g. the Go adapter's scalar-literal
|
|
451
|
+
* loop typing). Optional/best-effort — mirrors `IRExpression.parsed`;
|
|
452
|
+
* consumers fall back to parsing `array`.
|
|
453
|
+
*/
|
|
454
|
+
arrayParsed?: ParsedExpr
|
|
407
455
|
/** Pre-transformed array expr with destructured prop refs rewritten to _p.xxx. */
|
|
408
456
|
templateArray?: string
|
|
409
457
|
arrayType: TypeInfo | null
|
|
@@ -471,14 +519,16 @@ export interface IRLoop {
|
|
|
471
519
|
* When present, the loop renders with an if-condition wrapping each iteration.
|
|
472
520
|
* Example: todos.filter(t => !t.done).map(...) stores { param: 't', predicate: ParsedExpr, raw: '!t.done' }
|
|
473
521
|
*
|
|
474
|
-
*
|
|
522
|
+
* Block-body filters like
|
|
475
523
|
* filter(t => { const f = filter(); if (f === 'active') return !t.done; return true })
|
|
476
|
-
*
|
|
524
|
+
* are normalized to a single boolean `predicate` expression at IR-build time
|
|
525
|
+
* (#2040, `foldBlockToExpr` + `predicateTernaryToLogical` in `jsx-to-ir`), so
|
|
526
|
+
* adapters only ever see the unified expression form — there is no separate
|
|
527
|
+
* block-statement shape to lower.
|
|
477
528
|
*/
|
|
478
529
|
filterPredicate?: {
|
|
479
530
|
param: string
|
|
480
|
-
predicate?: ParsedExpr //
|
|
481
|
-
blockBody?: ParsedStatement[] // Block body (mutually exclusive with predicate)
|
|
531
|
+
predicate?: ParsedExpr // Boolean predicate expression (folded from any block body)
|
|
482
532
|
raw: string // Original string for error messages
|
|
483
533
|
}
|
|
484
534
|
|
|
@@ -487,14 +537,14 @@ export interface IRLoop {
|
|
|
487
537
|
* When present, the loop array is sorted before iteration.
|
|
488
538
|
* Example: todos.sort((a, b) => a.priority - b.priority).map(...)
|
|
489
539
|
*
|
|
490
|
-
* The
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
540
|
+
* The {@link IRLoopSort} struct carries the generic comparator `arrow`
|
|
541
|
+
* (params + body) the SSR adapter serializes to the runtime evaluator
|
|
542
|
+
* (eval-first; `sortComparatorFromArrow` fallback for `localeCompare`), plus
|
|
543
|
+
* the param names + raw body for the client JS round-trip. Lifted off the
|
|
544
|
+
* `.sort()` callback during `jsx-to-ir.ts` chain detection — see
|
|
545
|
+
* `extractSortComparator`. (#2018 P5)
|
|
496
546
|
*/
|
|
497
|
-
sortComparator?:
|
|
547
|
+
sortComparator?: IRLoopSort
|
|
498
548
|
|
|
499
549
|
/**
|
|
500
550
|
* When both filter and sort are chained, indicates the order of operations.
|
|
@@ -746,6 +796,13 @@ export interface IRIfStatement {
|
|
|
746
796
|
condition: string
|
|
747
797
|
/** Pre-transformed condition with destructured prop refs rewritten to _p.xxx. */
|
|
748
798
|
templateCondition?: string
|
|
799
|
+
/**
|
|
800
|
+
* Structured parse of `condition` (`parseExpression(condition.trim())`),
|
|
801
|
+
* attached during IR construction so adapters lower the condition from the
|
|
802
|
+
* tree instead of re-parsing the string. Optional/best-effort — see
|
|
803
|
+
* `IRExpression.parsed`; consumers fall back to parsing `condition`.
|
|
804
|
+
*/
|
|
805
|
+
parsedCondition?: ParsedExpr
|
|
749
806
|
/** The JSX return in the then branch */
|
|
750
807
|
consequent: IRNode
|
|
751
808
|
/** The else branch: either another IRIfStatement (else if) or IRNode (final else) */
|
|
@@ -839,6 +896,14 @@ export interface ExpressionAttr {
|
|
|
839
896
|
/** `expr` with destructured prop refs rewritten to `_p.xxx`, for SSR
|
|
840
897
|
* template inlining. Absent when no rewrite was needed. */
|
|
841
898
|
templateExpr?: string
|
|
899
|
+
/**
|
|
900
|
+
* Structured parse of `expr` (`parseExpression(expr.trim())`), attached
|
|
901
|
+
* during IR construction so adapters lower the attribute value from the tree
|
|
902
|
+
* instead of re-parsing the string (often several times per attribute).
|
|
903
|
+
* Optional/best-effort — see `IRExpression.parsed`; consumers fall back to
|
|
904
|
+
* parsing `expr`.
|
|
905
|
+
*/
|
|
906
|
+
parsed?: ParsedExpr
|
|
842
907
|
/** Set when the producer peeled an `expr || undefined` boolean-presence
|
|
843
908
|
* pattern; adapters fold this back into `(expr) || undefined` at emit. */
|
|
844
909
|
presenceOrUndefined?: boolean
|
|
@@ -873,6 +938,16 @@ export interface SpreadAttr {
|
|
|
873
938
|
kind: 'spread'
|
|
874
939
|
expr: string
|
|
875
940
|
templateExpr?: string
|
|
941
|
+
/**
|
|
942
|
+
* Structured parse of `expr` (`parseExpression(expr.trim())`), attached
|
|
943
|
+
* during IR construction so adapters lower the spread bag from the tree
|
|
944
|
+
* instead of re-parsing the string with `ts.createSourceFile`. Optional /
|
|
945
|
+
* best-effort — mirrors `ExpressionAttr.parsed`: it may be absent (a node the
|
|
946
|
+
* attach walk misses, or an empty `expr`), and parsing may yield
|
|
947
|
+
* `{ kind: 'unsupported' }`, which adapters treat as unlowerable and handle
|
|
948
|
+
* via their existing non-conditional spread paths (or BF101).
|
|
949
|
+
*/
|
|
950
|
+
parsed?: ParsedExpr
|
|
876
951
|
/**
|
|
877
952
|
* Component-scoped, stable slot ID assigned at IR-build time for
|
|
878
953
|
* adapters that need to plumb the spread bag through a structured
|
|
@@ -1039,6 +1114,14 @@ export interface SignalInfo {
|
|
|
1039
1114
|
getter: string
|
|
1040
1115
|
setter: string | null
|
|
1041
1116
|
initialValue: string
|
|
1117
|
+
/**
|
|
1118
|
+
* `initialValue` parsed into a structured tree (Roadmap A). Attached
|
|
1119
|
+
* best-effort by the analyzer so adapters can lower a literal initial value
|
|
1120
|
+
* (e.g. `useState(['a', 'b'])`) from structure instead of re-parsing the
|
|
1121
|
+
* string with `ts.createSourceFile`. Absent when the shape isn't supported;
|
|
1122
|
+
* consumers fall back to parsing `initialValue`.
|
|
1123
|
+
*/
|
|
1124
|
+
parsed?: ParsedExpr
|
|
1042
1125
|
/** Initial value with TypeScript type annotations preserved, for .tsx output */
|
|
1043
1126
|
typedInitialValue?: string
|
|
1044
1127
|
type: TypeInfo
|
|
@@ -1075,6 +1158,26 @@ export interface SignalInfo {
|
|
|
1075
1158
|
isModule?: boolean
|
|
1076
1159
|
/** When true, the declaration carries an `export` keyword. */
|
|
1077
1160
|
isExported?: boolean
|
|
1161
|
+
/**
|
|
1162
|
+
* Request-scoped environment-signal key when this signal was produced by an
|
|
1163
|
+
* env-signal factory (`createSearchParams()` → `'search'`), rather than by
|
|
1164
|
+
* `createSignal`. Set structurally by the analyzer (#2057) — the getter is a
|
|
1165
|
+
* normal reactive getter (so it lands in the fold purity oracle for free, no
|
|
1166
|
+
* name allow-list), but its *value* is a request-scoped reader with methods
|
|
1167
|
+
* (`.get(key)`), which adapters lower to their per-request reader object
|
|
1168
|
+
* instead of a plain template field. This flag is how adapters recognise an
|
|
1169
|
+
* env signal from structure instead of matching the import name.
|
|
1170
|
+
*/
|
|
1171
|
+
envReader?: string
|
|
1172
|
+
/**
|
|
1173
|
+
* For an env signal (`envReader` set), the exact callee text of its factory
|
|
1174
|
+
* call as written — `'createSearchParams'`, an alias (`'csp'` for
|
|
1175
|
+
* `import { createSearchParams as csp }`), or a namespace access
|
|
1176
|
+
* (`'bf.createSearchParams'`). Backends that re-emit the declaration (client
|
|
1177
|
+
* JS, JSX/Hono SSR) emit `<envFactory>()` so the call resolves to the binding
|
|
1178
|
+
* actually in scope, not a hardcoded canonical name (#2057).
|
|
1179
|
+
*/
|
|
1180
|
+
envFactory?: string
|
|
1078
1181
|
}
|
|
1079
1182
|
|
|
1080
1183
|
export interface MemoInfo {
|
|
@@ -1082,6 +1185,52 @@ export interface MemoInfo {
|
|
|
1082
1185
|
computation: string
|
|
1083
1186
|
/** Computation with TypeScript type annotations preserved, for .tsx output */
|
|
1084
1187
|
typedComputation?: string
|
|
1188
|
+
/**
|
|
1189
|
+
* Structured parse of the memo's BODY as a single value expression, computed
|
|
1190
|
+
* once at analysis time. Lets adapters pattern-match the memo's shape on the
|
|
1191
|
+
* structured tree instead of re-parsing `computation` with their own AST walks
|
|
1192
|
+
* / regexes.
|
|
1193
|
+
*
|
|
1194
|
+
* Set for an expression-bodied arrow (`() => <body>`) whose body
|
|
1195
|
+
* `parseExpression` supports, AND — since #2040 — for a block-bodied memo
|
|
1196
|
+
* (`() => { … }`) whose statements `foldBlockToExpr` can normalize to one
|
|
1197
|
+
* expression (`let`-inline + value `if` / early `return` → ternary). A block
|
|
1198
|
+
* the fold refuses (imperative residue) or a shape `parseExpression` can't
|
|
1199
|
+
* represent leaves `parsed` undefined, so consumers must still fall back to
|
|
1200
|
+
* `parsedBlock` / `computation` when it's missing. NOTE: a present `parsed`
|
|
1201
|
+
* therefore no longer implies an expression-bodied arrow.
|
|
1202
|
+
*/
|
|
1203
|
+
parsed?: ParsedExpr
|
|
1204
|
+
/**
|
|
1205
|
+
* Whether the memo's effective body is a template literal (`() => `…`` or a
|
|
1206
|
+
* block body whose first `return` is one), classified once at analysis time
|
|
1207
|
+
* from the real arrow AST. Lets the Go adapter pick the `string` field type
|
|
1208
|
+
* without re-parsing `computation` with `ts.createSourceFile`. A template
|
|
1209
|
+
* literal — including a no-substitution `` `plain` `` — folds to a plain
|
|
1210
|
+
* string `ParsedExpr` literal, losing the backtick distinction, so this is a
|
|
1211
|
+
* dedicated flag rather than a `parsed.kind` check.
|
|
1212
|
+
*/
|
|
1213
|
+
bodyIsTemplateLiteral?: boolean
|
|
1214
|
+
/**
|
|
1215
|
+
* A block-bodied memo's statements, parsed best-effort (tolerant: a statement
|
|
1216
|
+
* the parser can't represent is omitted). Lets the Go adapter pattern-match
|
|
1217
|
+
* block-body memo shapes — e.g. the `const k = getter(); if (!k) return CONST`
|
|
1218
|
+
* guard — on the structured statements instead of re-parsing `computation`
|
|
1219
|
+
* with `ts.createSourceFile`. Absent for expression-bodied memos (those carry
|
|
1220
|
+
* `parsed` instead) and when the arrow has no block body.
|
|
1221
|
+
*/
|
|
1222
|
+
parsedBlock?: ParsedStatement[]
|
|
1223
|
+
/**
|
|
1224
|
+
* Whether {@link parsedBlock} represents EVERY statement of the block (true)
|
|
1225
|
+
* or the tolerant parser omitted at least one it couldn't represent (false).
|
|
1226
|
+
* A consumer that must reason about the whole block — e.g. one that bails on
|
|
1227
|
+
* any statement it doesn't recognise (the template-literal memo lowering) —
|
|
1228
|
+
* reads this and falls back when it's `false`, since omitted statements are
|
|
1229
|
+
* otherwise invisible. Consumers that scan for a recognised prefix and ignore
|
|
1230
|
+
* the rest (the guard-and-return-const lowering) can disregard it. Only set
|
|
1231
|
+
* when `parsedBlock` is.
|
|
1232
|
+
*/
|
|
1233
|
+
parsedBlockComplete?: boolean
|
|
1085
1234
|
type: TypeInfo
|
|
1086
1235
|
deps: string[]
|
|
1087
1236
|
loc: SourceLocation
|
|
@@ -1282,6 +1431,17 @@ export interface FunctionInfo {
|
|
|
1282
1431
|
export interface ConstantInfo {
|
|
1283
1432
|
name: string
|
|
1284
1433
|
value?: string
|
|
1434
|
+
/**
|
|
1435
|
+
* `value` parsed into a structured tree (Roadmap A). Attached best-effort by
|
|
1436
|
+
* the analyzer (parsed from the parenthesised value so a bare object literal
|
|
1437
|
+
* — which TS reads as a block at statement position — resolves to an
|
|
1438
|
+
* `object-literal` rather than failing). Lets adapters lower a constant value
|
|
1439
|
+
* (e.g. a module-scope record's `{ … }`) from structure instead of
|
|
1440
|
+
* re-parsing the string with `ts.createSourceFile`. Absent when the constant
|
|
1441
|
+
* has no `value` string (e.g. an inlined-JSX const) or when the analyzer
|
|
1442
|
+
* couldn't structure it (best-effort — consumers fall back to the string).
|
|
1443
|
+
*/
|
|
1444
|
+
parsed?: ParsedExpr
|
|
1285
1445
|
/** Value with TypeScript type annotations preserved, for .tsx output */
|
|
1286
1446
|
typedValue?: string
|
|
1287
1447
|
valueBranches?: string[]
|
|
@@ -1367,6 +1527,16 @@ export interface IRMetadata {
|
|
|
1367
1527
|
* resolves the compiled module rather than the source `.tsx`.
|
|
1368
1528
|
*/
|
|
1369
1529
|
clientSignalImportSources?: Set<string>
|
|
1530
|
+
/**
|
|
1531
|
+
* Backend-neutral SSR seed plan: per-binding derived/opaque/env-reader
|
|
1532
|
+
* classification in declaration order, plus the base scope, computed by
|
|
1533
|
+
* `computeSsrSeedPlan` from this metadata. Attached by `buildMetadata` and
|
|
1534
|
+
* serialized into IR JSON like the rest of the metadata; template adapters
|
|
1535
|
+
* consume it instead of re-deriving scope/derivability themselves (their
|
|
1536
|
+
* target-syntax choices — lowering, self-shadow rules, constant-emit
|
|
1537
|
+
* guards — stay adapter-side).
|
|
1538
|
+
*/
|
|
1539
|
+
ssrSeedPlan?: SsrSeedPlan
|
|
1370
1540
|
}
|
|
1371
1541
|
|
|
1372
1542
|
// =============================================================================
|