@barefootjs/jsx 0.17.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 +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +47 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +921 -726
- package/dist/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/expression-parser.test.ts +11 -1
- package/src/__tests__/free-identifiers.test.ts +55 -0
- package/src/__tests__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +19 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/adapters/env-signal.ts +57 -9
- package/src/compiler.ts +6 -1
- package/src/expression-parser.ts +199 -9
- package/src/index.ts +7 -2
- package/src/ssr-seed-plan.ts +146 -0
- package/src/types.ts +11 -0
|
@@ -22,28 +22,76 @@ export const ENV_SIGNAL_CLIENT_FACTORY: Record<string, string> = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
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.
|
|
26
63
|
*
|
|
27
|
-
* Recognised **structurally** (#2057):
|
|
64
|
+
* Recognised **structurally** (#2057): an env signal is declared as a
|
|
28
65
|
* `createSignal`-shaped `const [searchParams, setSearchParams] =
|
|
29
66
|
* createSearchParams()`, so the analyzer collects it into `metadata.signals`
|
|
30
|
-
* with `envReader: '
|
|
67
|
+
* with `envReader: '<key>'` — exactly like any other signal, but tagged. This
|
|
31
68
|
* function returns those getters (whatever the destructured name is —
|
|
32
69
|
* `searchParams`, or an alias), so adapters match the reader `.get()` call
|
|
33
|
-
* against the binding actually used, with **no
|
|
34
|
-
*
|
|
70
|
+
* against the binding actually used, with **no name allow-list** (this
|
|
71
|
+
* supersedes the import-name matching, and the closed #2055).
|
|
35
72
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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).
|
|
38
79
|
*/
|
|
39
|
-
export function
|
|
80
|
+
export function envSignalLocalNames(metadata: IRMetadata, key?: string): Set<string> {
|
|
40
81
|
const names = new Set<string>()
|
|
41
82
|
for (const s of metadata.signals) {
|
|
42
|
-
if (s.envReader ===
|
|
83
|
+
if (s.envReader !== undefined && (key === undefined || s.envReader === key)) {
|
|
84
|
+
names.add(s.getter)
|
|
85
|
+
}
|
|
43
86
|
}
|
|
44
87
|
return names
|
|
45
88
|
}
|
|
46
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
|
+
|
|
47
95
|
/**
|
|
48
96
|
* True when the component declares the `searchParams` env signal. Convenience
|
|
49
97
|
* for adapters/harnesses that only need to gate on presence (the lowering
|
package/src/compiler.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { generateModuleExports, collectInlineExportedNames } from './module-expo
|
|
|
23
23
|
import { applyCssLayerPrefix } from './css-layer-prefixer.ts'
|
|
24
24
|
import { preprocessInlineJsxCallbacks } from './preprocess-inline-jsx-callbacks.ts'
|
|
25
25
|
import { extractSsrDefaults } from './ssr-defaults.ts'
|
|
26
|
+
import { computeSsrSeedPlan } from './ssr-seed-plan.ts'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Extended compile options with required adapter
|
|
@@ -482,7 +483,7 @@ function compileMultipleComponents(
|
|
|
482
483
|
export function buildMetadata(
|
|
483
484
|
ctx: ReturnType<typeof analyzeComponent>,
|
|
484
485
|
): IRMetadata {
|
|
485
|
-
|
|
486
|
+
const metadata: IRMetadata = {
|
|
486
487
|
componentName: ctx.componentName || 'Unknown',
|
|
487
488
|
hasDefaultExport: ctx.hasDefaultExport,
|
|
488
489
|
isExported: ctx.isExported,
|
|
@@ -512,6 +513,10 @@ export function buildMetadata(
|
|
|
512
513
|
localFunctions: ctx.localFunctions,
|
|
513
514
|
localConstants: ctx.localConstants,
|
|
514
515
|
}
|
|
516
|
+
// Computed from the assembled metadata (not `ctx`): the plan reads the
|
|
517
|
+
// exact fields adapters see, so the two can never disagree.
|
|
518
|
+
metadata.ssrSeedPlan = computeSsrSeedPlan(metadata)
|
|
519
|
+
return metadata
|
|
515
520
|
}
|
|
516
521
|
|
|
517
522
|
// =============================================================================
|
package/src/expression-parser.ts
CHANGED
|
@@ -247,8 +247,12 @@ export interface SupportResult {
|
|
|
247
247
|
const UNSUPPORTED_METHODS = new Set([
|
|
248
248
|
// Higher-order array methods. Seven of these (`filter`, `every`,
|
|
249
249
|
// `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
|
|
250
|
-
// intercepted as `higher-order` IR before reaching this gate
|
|
251
|
-
// `map` is intercepted as an IRLoop
|
|
250
|
+
// intercepted as `higher-order` IR before reaching this gate.
|
|
251
|
+
// `map` is intercepted as an IRLoop when its callback returns JSX,
|
|
252
|
+
// and as a `CALLBACK_METHODS` evaluator lowering (`map_eval`, #2073)
|
|
253
|
+
// when it returns a value — it stays listed here so the fall-throughs
|
|
254
|
+
// (a bare `arr.map` reference, a function-reference callback) still
|
|
255
|
+
// refuse loudly. `reduce` / `reduceRight` stay
|
|
252
256
|
// listed here so the shapes the Tier C catalogue can't lower still
|
|
253
257
|
// refuse loudly: the `convertNode` call branch intercepts a matching
|
|
254
258
|
// `.reduce(fn, init)` / `.reduceRight(fn, init)` into the structured
|
|
@@ -603,11 +607,12 @@ export function tsNodeToParsedExpr(node: ts.Node): ParsedExpr {
|
|
|
603
607
|
* Higher-order array methods whose callback body the runtime evaluator drives
|
|
604
608
|
* (#2018). Recognised generically as a `call` whose callee is `<recv>.<method>`
|
|
605
609
|
* and whose first argument is an `arrow`; the adapter serializes the arrow body
|
|
606
|
-
* to the evaluator.
|
|
607
|
-
*
|
|
610
|
+
* to the evaluator. A JSX-returning `.map` / `.flatMap` is an IRLoop upstream
|
|
611
|
+
* and never reaches this recognition; the value-returning `.map(cb)` form
|
|
612
|
+
* (e.g. `tags.map(t => \`#${t}\`).join(' ')`) lowers via `map_eval` (#2073).
|
|
608
613
|
*/
|
|
609
614
|
export const CALLBACK_METHODS: ReadonlySet<string> = new Set([
|
|
610
|
-
'filter', 'every', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex',
|
|
615
|
+
'filter', 'map', 'every', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex',
|
|
611
616
|
'sort', 'toSorted', 'reduce', 'reduceRight', 'flatMap',
|
|
612
617
|
])
|
|
613
618
|
|
|
@@ -2207,8 +2212,12 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2207
2212
|
// the receiver and the callback BODY are supported. Recognised before the
|
|
2208
2213
|
// `UNSUPPORTED_METHODS` gate so the eval-lowered shapes aren't refused
|
|
2209
2214
|
// (a BARE method reference — `arr.filter` uncalled, no arrow arg — still
|
|
2210
|
-
// falls through to the gate). A nested callback inside the body
|
|
2211
|
-
//
|
|
2215
|
+
// falls through to the gate). A nested callback inside the body is NOT
|
|
2216
|
+
// refused here: the evaluator refuses it (`serializeParsedExpr` → null)
|
|
2217
|
+
// and each adapter then either lowers it faithfully (Mojo's inline
|
|
2218
|
+
// `grep`, Go's `len (bf_filter_eval …)`) or surfaces BF101 at its
|
|
2219
|
+
// predicate fallback's exact degrade points (#2038) — a blanket refusal
|
|
2220
|
+
// here would break the faithful shapes (#1443 PR4).
|
|
2212
2221
|
const cb = asCallbackMethodCall(expr)
|
|
2213
2222
|
if (cb) {
|
|
2214
2223
|
const objSupport = checkSupport(cb.object)
|
|
@@ -3121,6 +3130,82 @@ export function stringifyParsedExpr(expr: ParsedExpr): string {
|
|
|
3121
3130
|
}
|
|
3122
3131
|
}
|
|
3123
3132
|
|
|
3133
|
+
/**
|
|
3134
|
+
* Rewrite every zero-arg `call` node whose callee is a bare identifier in
|
|
3135
|
+
* `names` into that identifier — `tag()` → `tag` — leaving everything else
|
|
3136
|
+
* untouched. Returns a new tree; the input is never mutated.
|
|
3137
|
+
*
|
|
3138
|
+
* Rationale: in an SSR seed/constructor context a signal/memo getter call
|
|
3139
|
+
* reads the already-computed SEEDED value, so `tag()` reduces to "the value
|
|
3140
|
+
* bound to `tag`". Materialising the call lets the runtime evaluator (which
|
|
3141
|
+
* refuses any non-builtin call, {@link toEvalNode}'s `evalBuiltinCalleeName`
|
|
3142
|
+
* gate) evaluate a predicate that reads sibling memos — e.g. a `.filter`
|
|
3143
|
+
* predicate `(p) => !tag() || p.tags.includes(tag())` — with the getter's
|
|
3144
|
+
* value supplied through the evaluator's `base_env` instead of an
|
|
3145
|
+
* unsupported call node. `names` is caller-supplied (typically the sibling
|
|
3146
|
+
* signals/memos seeded alongside the derived memo being lowered), so a call
|
|
3147
|
+
* to an unrelated function is left as a `call` node and still refused by the
|
|
3148
|
+
* evaluator's builtin gate if it reaches `serializeParsedExpr`.
|
|
3149
|
+
*/
|
|
3150
|
+
export function materializeGetterCalls(expr: ParsedExpr, names: ReadonlySet<string>): ParsedExpr {
|
|
3151
|
+
const rw = (e: ParsedExpr): ParsedExpr => materializeGetterCalls(e, names)
|
|
3152
|
+
switch (expr.kind) {
|
|
3153
|
+
case 'call':
|
|
3154
|
+
if (
|
|
3155
|
+
expr.args.length === 0 &&
|
|
3156
|
+
expr.callee.kind === 'identifier' &&
|
|
3157
|
+
names.has(expr.callee.name)
|
|
3158
|
+
) {
|
|
3159
|
+
return { kind: 'identifier', name: expr.callee.name }
|
|
3160
|
+
}
|
|
3161
|
+
return { kind: 'call', callee: rw(expr.callee), args: expr.args.map(rw) }
|
|
3162
|
+
case 'binary':
|
|
3163
|
+
return { kind: 'binary', op: expr.op, left: rw(expr.left), right: rw(expr.right) }
|
|
3164
|
+
case 'logical':
|
|
3165
|
+
return { kind: 'logical', op: expr.op, left: rw(expr.left), right: rw(expr.right) }
|
|
3166
|
+
case 'unary':
|
|
3167
|
+
return { kind: 'unary', op: expr.op, argument: rw(expr.argument) }
|
|
3168
|
+
case 'conditional':
|
|
3169
|
+
return {
|
|
3170
|
+
kind: 'conditional',
|
|
3171
|
+
test: rw(expr.test),
|
|
3172
|
+
consequent: rw(expr.consequent),
|
|
3173
|
+
alternate: rw(expr.alternate),
|
|
3174
|
+
}
|
|
3175
|
+
case 'member':
|
|
3176
|
+
return { kind: 'member', object: rw(expr.object), property: expr.property, computed: expr.computed }
|
|
3177
|
+
case 'index-access':
|
|
3178
|
+
return { kind: 'index-access', object: rw(expr.object), index: rw(expr.index) }
|
|
3179
|
+
case 'template-literal':
|
|
3180
|
+
return {
|
|
3181
|
+
kind: 'template-literal',
|
|
3182
|
+
parts: expr.parts.map(p => (p.type === 'string' ? p : { type: 'expression', expr: rw(p.expr) })),
|
|
3183
|
+
}
|
|
3184
|
+
case 'array-literal':
|
|
3185
|
+
return { kind: 'array-literal', elements: expr.elements.map(rw) }
|
|
3186
|
+
case 'array-method':
|
|
3187
|
+
// `flat`'s `args` is always `[]` (the depth is carried structurally in
|
|
3188
|
+
// `flatDepth`, not `args`) — still rewrite `object`, just skip the
|
|
3189
|
+
// `args.map` that every other method needs.
|
|
3190
|
+
if (expr.method === 'flat') return { ...expr, object: rw(expr.object) }
|
|
3191
|
+
return { ...expr, object: rw(expr.object), args: expr.args.map(rw) }
|
|
3192
|
+
case 'object-literal':
|
|
3193
|
+
return {
|
|
3194
|
+
kind: 'object-literal',
|
|
3195
|
+
raw: expr.raw,
|
|
3196
|
+
properties: expr.properties.map(p => ({ ...p, value: rw(p.value) })),
|
|
3197
|
+
}
|
|
3198
|
+
case 'arrow':
|
|
3199
|
+
return { kind: 'arrow', params: expr.params, body: rw(expr.body) }
|
|
3200
|
+
// Leaves / opaque shapes — nothing to rewrite.
|
|
3201
|
+
case 'identifier':
|
|
3202
|
+
case 'literal':
|
|
3203
|
+
case 'regex':
|
|
3204
|
+
case 'unsupported':
|
|
3205
|
+
return expr
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3124
3209
|
/**
|
|
3125
3210
|
* Serialize a pure-expression `ParsedExpr` (a higher-order callback body) into
|
|
3126
3211
|
* the minimal JSON the runtime evaluator consumes — the format pinned by the
|
|
@@ -3201,10 +3286,20 @@ export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): s
|
|
|
3201
3286
|
// carries the ref on its `value` identifier, which is visited here.)
|
|
3202
3287
|
for (const p of e.properties) visit(p.value)
|
|
3203
3288
|
return
|
|
3289
|
+
case 'array-method':
|
|
3290
|
+
// Only `.includes(x)` is serializable ({@link toEvalNode}); its
|
|
3291
|
+
// `object` (the receiver) and `args` (the needle) are the value
|
|
3292
|
+
// positions serialized, so visit both when the tree reaches here
|
|
3293
|
+
// with that method. Every other `array-method` is non-serializable
|
|
3294
|
+
// and doesn't occur in a serializable body.
|
|
3295
|
+
if (e.method === 'includes') {
|
|
3296
|
+
visit(e.object)
|
|
3297
|
+
e.args.forEach(visit)
|
|
3298
|
+
}
|
|
3299
|
+
return
|
|
3204
3300
|
// Non-serializable kinds don't occur in a serializable body
|
|
3205
3301
|
// (serializeParsedExpr returns null for them); nothing to collect.
|
|
3206
3302
|
case 'literal':
|
|
3207
|
-
case 'array-method':
|
|
3208
3303
|
case 'arrow':
|
|
3209
3304
|
case 'regex':
|
|
3210
3305
|
case 'unsupported':
|
|
@@ -3215,6 +3310,87 @@ export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): s
|
|
|
3215
3310
|
return [...found].sort()
|
|
3216
3311
|
}
|
|
3217
3312
|
|
|
3313
|
+
/**
|
|
3314
|
+
* Every value-position identifier in `expr` NOT bound by an enclosing arrow's
|
|
3315
|
+
* own parameters — with proper lexical scoping: an arrow's params bind only
|
|
3316
|
+
* within that arrow's body, and nested arrows accumulate onto the enclosing
|
|
3317
|
+
* bound set. Unlike {@link freeVarsInBody} (which assumes a single flat param
|
|
3318
|
+
* set and never recurses into nested `arrow` nodes, since a serializable
|
|
3319
|
+
* evaluator body never contains one), this walks the full source-level tree —
|
|
3320
|
+
* including arrows — so a caller can ask "is this name free ANYWHERE in the
|
|
3321
|
+
* expression, honoring each arrow's own scope" rather than only within one
|
|
3322
|
+
* callback body.
|
|
3323
|
+
*
|
|
3324
|
+
* Walks the same value positions as {@link serializeParsedExpr} /
|
|
3325
|
+
* {@link freeVarsInBody}: call callee (skipped when it resolves to an
|
|
3326
|
+
* evaluator builtin — see {@link evalBuiltinCalleeName} — so `Math.floor(x)`
|
|
3327
|
+
* doesn't report `Math` as free) + args, binary/logical/unary operands,
|
|
3328
|
+
* conditional branches, a member's OBJECT only (the property name is not a
|
|
3329
|
+
* reference), an index-access's object + index, template-literal expression
|
|
3330
|
+
* parts, array-literal elements, array-method object + args, and an
|
|
3331
|
+
* object-literal's property VALUES (not keys). An `arrow` recurses into its
|
|
3332
|
+
* body with its own params added to the bound set.
|
|
3333
|
+
*
|
|
3334
|
+
* Returns `null` when the tree contains an `unsupported` node (or any other
|
|
3335
|
+
* shape this walk can't analyze) — the caller must fail safe rather than
|
|
3336
|
+
* assume nothing is free.
|
|
3337
|
+
*/
|
|
3338
|
+
export function freeIdentifiers(expr: ParsedExpr): Set<string> | null {
|
|
3339
|
+
const free = new Set<string>()
|
|
3340
|
+
|
|
3341
|
+
function visit(e: ParsedExpr, bound: ReadonlySet<string>): boolean {
|
|
3342
|
+
switch (e.kind) {
|
|
3343
|
+
case 'literal':
|
|
3344
|
+
case 'regex':
|
|
3345
|
+
return true
|
|
3346
|
+
case 'identifier':
|
|
3347
|
+
if (!bound.has(e.name)) free.add(e.name)
|
|
3348
|
+
return true
|
|
3349
|
+
case 'call': {
|
|
3350
|
+
const isBuiltinCallee = evalBuiltinCalleeName(e.callee) !== null
|
|
3351
|
+
if (!isBuiltinCallee && !visit(e.callee, bound)) return false
|
|
3352
|
+
for (const a of e.args) if (!visit(a, bound)) return false
|
|
3353
|
+
return true
|
|
3354
|
+
}
|
|
3355
|
+
case 'member':
|
|
3356
|
+
return visit(e.object, bound)
|
|
3357
|
+
case 'index-access':
|
|
3358
|
+
return visit(e.object, bound) && visit(e.index, bound)
|
|
3359
|
+
case 'binary':
|
|
3360
|
+
case 'logical':
|
|
3361
|
+
return visit(e.left, bound) && visit(e.right, bound)
|
|
3362
|
+
case 'unary':
|
|
3363
|
+
return visit(e.argument, bound)
|
|
3364
|
+
case 'conditional':
|
|
3365
|
+
return visit(e.test, bound) && visit(e.consequent, bound) && visit(e.alternate, bound)
|
|
3366
|
+
case 'template-literal':
|
|
3367
|
+
for (const p of e.parts) {
|
|
3368
|
+
if (p.type === 'expression' && !visit(p.expr, bound)) return false
|
|
3369
|
+
}
|
|
3370
|
+
return true
|
|
3371
|
+
case 'array-literal':
|
|
3372
|
+
for (const el of e.elements) if (!visit(el, bound)) return false
|
|
3373
|
+
return true
|
|
3374
|
+
case 'array-method':
|
|
3375
|
+
if (!visit(e.object, bound)) return false
|
|
3376
|
+
for (const a of e.args) if (!visit(a, bound)) return false
|
|
3377
|
+
return true
|
|
3378
|
+
case 'object-literal':
|
|
3379
|
+
for (const p of e.properties) if (!visit(p.value, bound)) return false
|
|
3380
|
+
return true
|
|
3381
|
+
case 'arrow': {
|
|
3382
|
+
const inner = new Set(bound)
|
|
3383
|
+
for (const p of e.params) inner.add(p)
|
|
3384
|
+
return visit(e.body, inner)
|
|
3385
|
+
}
|
|
3386
|
+
case 'unsupported':
|
|
3387
|
+
return false
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
return visit(expr, new Set()) ? free : null
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3218
3394
|
// Operators the evaluator implements (Go `eval.go` evalBinary / evalUnary, Perl
|
|
3219
3395
|
// `Evaluator.pm` _binary / _unary). An op outside these sets — loose `==`,
|
|
3220
3396
|
// `instanceof`, `**`, bitwise/shift, or the parser's `'unknown'` sentinel — is
|
|
@@ -3346,10 +3522,24 @@ function toEvalNode(e: ParsedExpr): Record<string, unknown> | null {
|
|
|
3346
3522
|
}
|
|
3347
3523
|
return { kind: 'object-literal', properties }
|
|
3348
3524
|
}
|
|
3525
|
+
case 'array-method': {
|
|
3526
|
+
// `.includes(x)` is the one `array-method` the evaluator executes
|
|
3527
|
+
// (Go `eval.go` / Perl `Evaluator.pm`, includes support): the
|
|
3528
|
+
// receiver-type dispatch (array SameValueZero membership vs string
|
|
3529
|
+
// substring) happens at evaluator runtime, same as the SSR template
|
|
3530
|
+
// lowering's `bf_includes` / `$bf->includes`. Every other
|
|
3531
|
+
// `array-method` (`join`, `slice`, `flat`, …) is outside the
|
|
3532
|
+
// evaluator's surface and refuses below.
|
|
3533
|
+
if (e.method === 'includes' && e.args.length === 1) {
|
|
3534
|
+
const object = toEvalNode(e.object)
|
|
3535
|
+
const arg = toEvalNode(e.args[0])
|
|
3536
|
+
return object && arg ? { kind: 'array-method', method: 'includes', object, args: [arg] } : null
|
|
3537
|
+
}
|
|
3538
|
+
return null
|
|
3539
|
+
}
|
|
3349
3540
|
// Outside the evaluator's pure-expression surface — refuse so the caller
|
|
3350
3541
|
// falls back to BF101 / `@client`. A nested `arrow` (a callback inside the
|
|
3351
3542
|
// body) is refused here, keeping the evaluator non-recursive.
|
|
3352
|
-
case 'array-method':
|
|
3353
3543
|
case 'arrow':
|
|
3354
3544
|
case 'regex':
|
|
3355
3545
|
case 'unsupported':
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ export type { CompileResult, CompileOptions, CompileOptionsWithAdapter, FileOutp
|
|
|
12
12
|
export { extractSsrDefaults } from './ssr-defaults.ts'
|
|
13
13
|
export type { SsrDefault } from './ssr-defaults.ts'
|
|
14
14
|
|
|
15
|
+
// Backend-neutral SSR seed plan (in-template derived signal/memo seeding)
|
|
16
|
+
export { computeSsrSeedPlan } from './ssr-seed-plan.ts'
|
|
17
|
+
export type { SsrSeedPlan, SsrSeedStep } from './ssr-seed-plan.ts'
|
|
18
|
+
|
|
15
19
|
// Pure IR types
|
|
16
20
|
export type {
|
|
17
21
|
ComponentIR,
|
|
@@ -77,7 +81,8 @@ export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
|
77
81
|
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
78
82
|
export { emitParsedExpr } from './adapters/parsed-expr-emitter.ts'
|
|
79
83
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
80
|
-
export { importsSearchParams, searchParamsLocalNames, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
84
|
+
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
85
|
+
export type { EnvSignalReader } from './adapters/env-signal.ts'
|
|
81
86
|
export { matchQueryHrefCall, queryHrefArgs, type QueryHrefCall, type QueryHrefTriple } from './query-href-lowering.ts'
|
|
82
87
|
export {
|
|
83
88
|
registerLoweringPlugin,
|
|
@@ -276,7 +281,7 @@ export {
|
|
|
276
281
|
export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
|
|
277
282
|
|
|
278
283
|
// Expression Parser
|
|
279
|
-
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
284
|
+
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
280
285
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
281
286
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
282
287
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
@@ -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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ParsedExpr, ParsedStatement } from './expression-parser.ts'
|
|
8
|
+
import type { SsrSeedPlan } from './ssr-seed-plan.ts'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Loop-hoisted sort comparator for the `.sort().map()` / `.toSorted().map()`
|
|
@@ -1526,6 +1527,16 @@ export interface IRMetadata {
|
|
|
1526
1527
|
* resolves the compiled module rather than the source `.tsx`.
|
|
1527
1528
|
*/
|
|
1528
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
|
|
1529
1540
|
}
|
|
1530
1541
|
|
|
1531
1542
|
// =============================================================================
|