@barefootjs/jsx 0.31.8 → 0.31.10
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/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +20 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +226 -66
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +66 -38
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +15 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts +10 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +210 -210
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +8 -2
- package/src/__tests__/csr-template-loop-shadowing.test.ts +5 -2
- package/src/__tests__/date-lowering.test.ts +1 -1
- package/src/__tests__/env-signal-template-prelude.test.ts +4 -2
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +135 -0
- package/src/__tests__/markup-prop-brand.test.ts +161 -0
- package/src/__tests__/mathml-mapArray-namespace.test.ts +230 -0
- package/src/__tests__/props-destructuring.test.ts +40 -3
- package/src/__tests__/ssr-defaults.test.ts +230 -0
- package/src/__tests__/ssr-seed-plan.test.ts +72 -0
- package/src/__tests__/text-slot-escaping.test.ts +21 -12
- package/src/analyzer.ts +83 -26
- package/src/expression-parser.ts +10 -1
- package/src/ir-to-client-js/collect-elements.ts +38 -5
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +10 -10
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +3 -4
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +18 -14
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +142 -78
- package/src/ir-to-client-js/emit-registration.ts +5 -1
- package/src/ir-to-client-js/html-template.ts +103 -12
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +6 -1
- package/src/ssr-defaults.ts +171 -6
- package/src/ssr-seed-plan.ts +82 -2
package/src/ssr-defaults.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// `undef`, which Mojo renders as empty string).
|
|
31
31
|
|
|
32
32
|
import ts from 'typescript'
|
|
33
|
+
import { extractFreeIdentifiersFromNode } from './analyzer.ts'
|
|
33
34
|
import type { IRMetadata } from './types.ts'
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -52,6 +53,16 @@ export interface SsrDefault {
|
|
|
52
53
|
* (`{ variant = 'default' }`) where the template variable maps 1:1
|
|
53
54
|
* to a caller-supplied prop. Omitted for signal / memo entries that
|
|
54
55
|
* are internal to the component.
|
|
56
|
+
*
|
|
57
|
+
* Invariant (#2669): a signal / memo entry carries `propName` IF AND
|
|
58
|
+
* ONLY IF it's a self-derivation collision — the signal/memo's own
|
|
59
|
+
* template variable shares a name with the prop its initializer
|
|
60
|
+
* derives from (`const [label] = createSignal(props.label ?? 'x')`).
|
|
61
|
+
* Every OTHER signal/memo entry is `propName`-less. Consumers (e.g.
|
|
62
|
+
* every template-stash adapter's conformance harness, `test-render.ts`)
|
|
63
|
+
* rely on this to know a signal/memo's stash seed must defer to the
|
|
64
|
+
* already-derived prop value rather than re-seeding from its own
|
|
65
|
+
* evaluated initial — see `extractSsrDefaults`'s signal/memo loops.
|
|
55
66
|
*/
|
|
56
67
|
propName?: string
|
|
57
68
|
/**
|
|
@@ -204,6 +215,11 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
|
|
|
204
215
|
// fed into the bindings map so subsequent memos can reference earlier
|
|
205
216
|
// signals (Counter's `doubled = createMemo(() => count() * 2)`).
|
|
206
217
|
const bindings: Record<string, EvalResult> = {}
|
|
218
|
+
// Component-scope const locals, for the transitive prop-reference
|
|
219
|
+
// resolution both `referencesOwnProp` calls below and the bare-props
|
|
220
|
+
// safety net (further down) use — computed once so every caller shares
|
|
221
|
+
// the same table.
|
|
222
|
+
const localConsts = localConstValuesByName(metadata)
|
|
207
223
|
|
|
208
224
|
// (#checkbox) Seed module-scope constants so a memo template-literal that
|
|
209
225
|
// references them resolves to a concrete string. Checkbox's `classes` memo
|
|
@@ -225,14 +241,73 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
|
|
|
225
241
|
// baked initial value.
|
|
226
242
|
if (sig.envReader) continue
|
|
227
243
|
const value = tryStaticEval(sig.initialValue, { bindings, propsLike })
|
|
228
|
-
|
|
244
|
+
// Self-derivation collision (#2669): a signal whose getter shares its
|
|
245
|
+
// name with the prop its OWN initializer derives from
|
|
246
|
+
// (`const [label] = createSignal(props.label ?? 'Default')`) gets a
|
|
247
|
+
// template-stash variable that the emitted template both READS (as the
|
|
248
|
+
// raw caller prop) and OVERWRITES (with the derived signal value) under
|
|
249
|
+
// that SAME name — see the Mojo/Jinja/etc. emitter's `{% set label =
|
|
250
|
+
// (label if label is defined else 'Default') %}`. Seeding the stash
|
|
251
|
+
// with the DERIVED value here would either permanently lock out a
|
|
252
|
+
// caller-supplied prop (idempotent derivations like `?? 'Default'` hide
|
|
253
|
+
// this — a caller's real value can never win) or double-apply a
|
|
254
|
+
// non-idempotent derivation (`(props.count ?? 1) * 2` seeded with the
|
|
255
|
+
// evaluated `2` re-derives to `2 * 2 = 4`). The correct seed is the RAW
|
|
256
|
+
// prop, so the entry must be a PROP entry (`propName` set, `value:
|
|
257
|
+
// null`) instead of a plain signal-value entry — the template's own
|
|
258
|
+
// `?? <default>` / `is not none` guard performs the real derivation,
|
|
259
|
+
// exactly like the bare-props safety net below.
|
|
260
|
+
//
|
|
261
|
+
// NOT self-derived (the initializer references a DIFFERENT prop, or no
|
|
262
|
+
// prop at all) keeps today's behavior: the signal's evaluated value
|
|
263
|
+
// wins the entry outright. This also correctly leaves alone the
|
|
264
|
+
// separate (out-of-scope) case where the signal's OWN initializer does
|
|
265
|
+
// NOT reference the same-named prop but the JSX body separately reads
|
|
266
|
+
// both `label()` and `props.label` — that's a template-variable-
|
|
267
|
+
// aliasing defect, not this one, and must render byte-identically.
|
|
268
|
+
//
|
|
269
|
+
// Invariant this establishes (relied on by every template-stash
|
|
270
|
+
// conformance harness's seeding loops — see `test-render.ts`): a
|
|
271
|
+
// signal/memo entry carries `propName` IF AND ONLY IF it went through
|
|
272
|
+
// this collision path. An ordinary signal/memo entry never has
|
|
273
|
+
// `propName` — that's exclusively how a harness (or a production
|
|
274
|
+
// manifest consumer) can tell "this local's stash seed must come from
|
|
275
|
+
// the caller-facing prop, not the local's own evaluated value" apart
|
|
276
|
+
// from "this is an internal signal/memo the caller cannot override".
|
|
277
|
+
if (metadata.propsObjectName !== null && referencesOwnProp(sig.initialValue, metadata.propsObjectName, sig.getter, localConsts)) {
|
|
278
|
+
// Pass 1 above already wrote the prop entry when the prop is
|
|
279
|
+
// *declared* on the props type — leave it as-is. An undeclared
|
|
280
|
+
// (untyped / inline-typed) prop has no pass-1 entry yet; create it
|
|
281
|
+
// here so the collision is still resolved for that shape.
|
|
282
|
+
if (!(sig.getter in out)) {
|
|
283
|
+
out[sig.getter] = { propName: sig.getter, value: null }
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
out[sig.getter] = { value: resultToJsonable(value) }
|
|
287
|
+
}
|
|
288
|
+
// `bindings` always gets the EVALUATED value regardless of which stash
|
|
289
|
+
// seed the entry above ended up with — a later memo referencing this
|
|
290
|
+
// getter (`createMemo(() => label() + '!')`) must keep resolving
|
|
291
|
+
// through the chain exactly as before; only the manifest's own seed
|
|
292
|
+
// choice changes, not the static-eval semantics.
|
|
229
293
|
bindings[sig.getter] = value
|
|
230
294
|
}
|
|
231
295
|
|
|
232
296
|
for (const memo of metadata.memos) {
|
|
233
297
|
if (memo.isModule) continue
|
|
234
298
|
const value = tryStaticEval(memo.computation, { bindings, propsLike })
|
|
235
|
-
|
|
299
|
+
// Same self-derivation collision as signals above, for a memo whose
|
|
300
|
+
// computation derives from a same-named prop
|
|
301
|
+
// (`createMemo(() => (props.label ?? 'Default') + n())`). See the
|
|
302
|
+
// signal loop's comment for the full mechanism and the `propName`
|
|
303
|
+
// invariant this relies on.
|
|
304
|
+
if (metadata.propsObjectName !== null && referencesOwnProp(memo.computation, metadata.propsObjectName, memo.name, localConsts)) {
|
|
305
|
+
if (!(memo.name in out)) {
|
|
306
|
+
out[memo.name] = { propName: memo.name, value: null }
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
out[memo.name] = { value: resultToJsonable(value) }
|
|
310
|
+
}
|
|
236
311
|
bindings[memo.name] = value
|
|
237
312
|
}
|
|
238
313
|
|
|
@@ -251,11 +326,11 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
|
|
|
251
326
|
const referenced = new Set<string>()
|
|
252
327
|
for (const sig of metadata.signals) {
|
|
253
328
|
if (!sig.getter || sig.isModule || sig.envReader) continue
|
|
254
|
-
|
|
329
|
+
collectPropRefsTransitive(sig.initialValue, metadata.propsObjectName, localConsts, referenced)
|
|
255
330
|
}
|
|
256
331
|
for (const memo of metadata.memos) {
|
|
257
332
|
if (memo.isModule) continue
|
|
258
|
-
|
|
333
|
+
collectPropRefsTransitive(memo.computation, metadata.propsObjectName, localConsts, referenced)
|
|
259
334
|
}
|
|
260
335
|
for (const name of referenced) {
|
|
261
336
|
// Don't clobber a signal / memo (or already-seeded prop) of the same
|
|
@@ -268,6 +343,47 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
|
|
|
268
343
|
return Object.keys(out).length === 0 ? undefined : out
|
|
269
344
|
}
|
|
270
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Component-scope (non-module) `const` locals, by name → value source text.
|
|
348
|
+
* `referencesOwnProp` and `collectPropRefsTransitive` (below) use this to
|
|
349
|
+
* see PAST one hop of pure indirection between a prop read and the
|
|
350
|
+
* signal/memo that derives from it — `const mid = props.label;
|
|
351
|
+
* createSignal(mid ?? 'Default')` (#2685 review, one hop past #2669's
|
|
352
|
+
* direct-access-only detection). Module-scope consts are excluded: a
|
|
353
|
+
* MODULE const's value is fixed at compile time and isn't a `props.X` read
|
|
354
|
+
* (a module const referencing `props` isn't legal JS — `props` is a
|
|
355
|
+
* function parameter), so it can never contribute a prop reference here.
|
|
356
|
+
*/
|
|
357
|
+
function localConstValuesByName(metadata: IRMetadata): ReadonlyMap<string, string> {
|
|
358
|
+
const out = new Map<string, string>()
|
|
359
|
+
for (const c of metadata.localConstants ?? []) {
|
|
360
|
+
if (c.isModule || c.value === undefined) continue
|
|
361
|
+
out.set(c.name, c.value)
|
|
362
|
+
}
|
|
363
|
+
return out
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Does `expr` (a signal initializer or memo computation) read
|
|
368
|
+
* `propsObjectName.<name>` where `name` is that SAME signal's getter / that
|
|
369
|
+
* SAME memo's name — directly, or through a chain of component-scope
|
|
370
|
+
* `const` locals? This is the #2669 self-derivation test (widened by
|
|
371
|
+
* #2685 review to see through indirection): reuses `collectPropRefsTransitive`
|
|
372
|
+
* (never a bespoke walker — see the CLAUDE.md rule this file already
|
|
373
|
+
* follows for `props.X` collection) and just checks membership of the one
|
|
374
|
+
* name we care about.
|
|
375
|
+
*/
|
|
376
|
+
function referencesOwnProp(
|
|
377
|
+
expr: string | undefined,
|
|
378
|
+
propsObjectName: string,
|
|
379
|
+
name: string,
|
|
380
|
+
localConsts: ReadonlyMap<string, string>,
|
|
381
|
+
): boolean {
|
|
382
|
+
const referenced = new Set<string>()
|
|
383
|
+
collectPropRefsTransitive(expr, propsObjectName, localConsts, referenced)
|
|
384
|
+
return referenced.has(name)
|
|
385
|
+
}
|
|
386
|
+
|
|
271
387
|
/**
|
|
272
388
|
* Collect the first-level property name of every `propsObjectName.X` access
|
|
273
389
|
* within `expr` (e.g. `props.initial ?? 0` → `initial`). Used to seed the
|
|
@@ -276,6 +392,11 @@ export function extractSsrDefaults(metadata: IRMetadata): Record<string, SsrDefa
|
|
|
276
392
|
* prop `a`: adapters lower that to `$a->{b}`, so the bare `$a` needs
|
|
277
393
|
* seeding just the same — the walk stops at the first
|
|
278
394
|
* `propsObjectName.<name>` match and collects `a` (not `b`).
|
|
395
|
+
*
|
|
396
|
+
* Direct-access only — does NOT look through a component-scope `const`
|
|
397
|
+
* that itself reads `propsObjectName.X`. Callers that need to see past
|
|
398
|
+
* that indirection use `collectPropRefsTransitive` below, which wraps this
|
|
399
|
+
* function rather than duplicating its walk.
|
|
279
400
|
*/
|
|
280
401
|
function collectPropRefs(
|
|
281
402
|
expr: string | undefined,
|
|
@@ -304,6 +425,46 @@ function collectPropRefs(
|
|
|
304
425
|
visit(node)
|
|
305
426
|
}
|
|
306
427
|
|
|
428
|
+
/**
|
|
429
|
+
* `collectPropRefs`, widened to look THROUGH component-scope `const`
|
|
430
|
+
* locals (#2685 review): a signal/memo initializer often reads a prop one
|
|
431
|
+
* hop removed — `const mid = props.label; createSignal(mid ?? 'Default')`
|
|
432
|
+
* — rather than `propsObjectName.X` directly. Runs the existing
|
|
433
|
+
* direct-access walk over `expr` first (unchanged behavior for the common
|
|
434
|
+
* case), then finds every VALUE-POSITION identifier `expr` references
|
|
435
|
+
* (via the analyzer's `extractFreeIdentifiersFromNode` — never a bespoke
|
|
436
|
+
* identifier scan, so a property-access tail like `foo.mid` or an
|
|
437
|
+
* object-literal key like `{ mid: 1 }` is correctly NOT mistaken for a
|
|
438
|
+
* read of a local named `mid`) and, for each one that names a local
|
|
439
|
+
* const, recurses into THAT const's own value expression the same way.
|
|
440
|
+
*
|
|
441
|
+
* `visited` guards re-entering the same const twice — both for the
|
|
442
|
+
* (source-impossible, since JS TDZ forbids a const referencing itself)
|
|
443
|
+
* pathological-cycle case, and for the ordinary diamond case (two
|
|
444
|
+
* branches of `expr` both reading the same local) so it isn't walked
|
|
445
|
+
* twice. Fresh per top-level call via the default parameter, so unrelated
|
|
446
|
+
* calls for different signals/memos don't share state.
|
|
447
|
+
*/
|
|
448
|
+
function collectPropRefsTransitive(
|
|
449
|
+
expr: string | undefined,
|
|
450
|
+
propsObjectName: string,
|
|
451
|
+
localConsts: ReadonlyMap<string, string>,
|
|
452
|
+
out: Set<string>,
|
|
453
|
+
visited: Set<string> = new Set(),
|
|
454
|
+
): void {
|
|
455
|
+
if (!expr || !expr.trim()) return
|
|
456
|
+
collectPropRefs(expr, propsObjectName, out)
|
|
457
|
+
const node = parseExpression(expr)
|
|
458
|
+
if (!node) return
|
|
459
|
+
for (const id of extractFreeIdentifiersFromNode(node)) {
|
|
460
|
+
if (visited.has(id)) continue
|
|
461
|
+
const constValue = localConsts.get(id)
|
|
462
|
+
if (constValue === undefined) continue
|
|
463
|
+
visited.add(id)
|
|
464
|
+
collectPropRefsTransitive(constValue, propsObjectName, localConsts, out, visited)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
307
468
|
function resultToJsonable(v: EvalResult): unknown {
|
|
308
469
|
if (v === UNRESOLVED) return null
|
|
309
470
|
if (v === undefined) return null
|
|
@@ -366,12 +527,16 @@ function evalStatementsForReturn(
|
|
|
366
527
|
|
|
367
528
|
function parseExpression(expr: string): ts.Expression | null {
|
|
368
529
|
// Wrap in parens so a leading `{}` parses as an object literal rather
|
|
369
|
-
// than an empty block statement.
|
|
530
|
+
// than an empty block statement. Parent nodes ARE set (unlike a bare
|
|
531
|
+
// parse) so `collectPropRefsTransitive` can call the analyzer's
|
|
532
|
+
// `extractFreeIdentifiersFromNode` — which needs `.parent` to tell a
|
|
533
|
+
// value-position identifier from a property-access tail / object-literal
|
|
534
|
+
// key / parameter name — on the result.
|
|
370
535
|
const sf = ts.createSourceFile(
|
|
371
536
|
'__ssr_default__.ts',
|
|
372
537
|
`(${expr})`,
|
|
373
538
|
ts.ScriptTarget.Latest,
|
|
374
|
-
|
|
539
|
+
true,
|
|
375
540
|
ts.ScriptKind.TS,
|
|
376
541
|
)
|
|
377
542
|
const stmt = sf.statements[0]
|
package/src/ssr-seed-plan.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { envSignalReaderFor, type EnvSignalReader } from './adapters/env-signal.
|
|
|
35
35
|
import {
|
|
36
36
|
extractArrowBodyExpression,
|
|
37
37
|
freeIdentifiers,
|
|
38
|
+
inlineBinding,
|
|
38
39
|
isSupported,
|
|
39
40
|
parseExpression,
|
|
40
41
|
type ParsedExpr,
|
|
@@ -97,6 +98,72 @@ function classify(
|
|
|
97
98
|
return { kind: 'derived', name, origin, expr, parsed, frees: [...frees] }
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Component-scope (non-module) `const` locals, by name, parsed to their
|
|
103
|
+
* `ParsedExpr` value — the substitution table `resolveThroughLocalConsts`
|
|
104
|
+
* inlines through. Module-scope consts are excluded: they're already part
|
|
105
|
+
* of `baseScope` (every adapter compile-time-inlines them to their literal
|
|
106
|
+
* value, so a reference to one is never a template-variable read — see the
|
|
107
|
+
* module doc), and a `let` has no stable value to substitute.
|
|
108
|
+
*/
|
|
109
|
+
function localConstExprsByName(metadata: IRMetadata): ReadonlyMap<string, ParsedExpr> {
|
|
110
|
+
const out = new Map<string, ParsedExpr>()
|
|
111
|
+
for (const c of metadata.localConstants ?? []) {
|
|
112
|
+
if (c.isModule || c.declarationKind !== 'const' || c.value === undefined) continue
|
|
113
|
+
out.set(c.name, c.parsed ?? parseExpression(c.value.trim()))
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a signal/memo's parsed expression through any component-scope
|
|
120
|
+
* `const` locals it references, so a hop of pure indirection between a
|
|
121
|
+
* prop read and its derived signal/memo — `const mid = props.label;
|
|
122
|
+
* createSignal(mid ?? 'Default')` — classifies from the SAME scope its
|
|
123
|
+
* fully-inlined form (`props.label ?? 'Default'`) would (#2685 review, the
|
|
124
|
+
* jsx-side twin of `ssr-defaults.ts`'s own `collectPropRefs` closure).
|
|
125
|
+
*
|
|
126
|
+
* Structural substitution only (never string splicing, per CLAUDE.md):
|
|
127
|
+
* reuses `inlineBinding`, the exact "let-inline" step `foldBlockToExpr`
|
|
128
|
+
* already performs for a block-bodied memo's own internal `const`s. A
|
|
129
|
+
* chained const (`const a = props.x; const b = a; createSignal(b ?? 1)`)
|
|
130
|
+
* closes the same way — each pass substitutes whatever local consts are
|
|
131
|
+
* still free in the current form, so `b` resolves to `a` first and `a`
|
|
132
|
+
* resolves to `props.x` on the next pass. Bounded to `localConsts.size + 1`
|
|
133
|
+
* iterations (the longest possible chain) purely as a cycle guard; a real
|
|
134
|
+
* cycle is impossible in valid source (JS TDZ forbids a const referencing
|
|
135
|
+
* itself, directly or transitively).
|
|
136
|
+
*
|
|
137
|
+
* `inlineBinding` returns `null` only on a capture hazard (the const's own
|
|
138
|
+
* free variable would be shadowed by a nested callback parameter of the
|
|
139
|
+
* same name inside the current form) — that occurrence is left un-inlined,
|
|
140
|
+
* so its name simply stays free and later fails `classify`'s availability
|
|
141
|
+
* check like any other out-of-scope reference (fails safe to `opaque`,
|
|
142
|
+
* never a wrong substitution).
|
|
143
|
+
*/
|
|
144
|
+
function resolveThroughLocalConsts(
|
|
145
|
+
parsed: ParsedExpr,
|
|
146
|
+
localConsts: ReadonlyMap<string, ParsedExpr>,
|
|
147
|
+
): ParsedExpr {
|
|
148
|
+
let current = parsed
|
|
149
|
+
const maxIter = localConsts.size + 1
|
|
150
|
+
for (let i = 0; i < maxIter; i++) {
|
|
151
|
+
const frees = freeIdentifiers(current)
|
|
152
|
+
if (frees === null) break
|
|
153
|
+
let changed = false
|
|
154
|
+
for (const name of frees) {
|
|
155
|
+
const value = localConsts.get(name)
|
|
156
|
+
if (!value) continue
|
|
157
|
+
const inlined = inlineBinding(current, name, value)
|
|
158
|
+
if (inlined === null) continue
|
|
159
|
+
current = inlined
|
|
160
|
+
changed = true
|
|
161
|
+
}
|
|
162
|
+
if (!changed) break
|
|
163
|
+
}
|
|
164
|
+
return current
|
|
165
|
+
}
|
|
166
|
+
|
|
100
167
|
/**
|
|
101
168
|
* Compute the component's SSR seed plan from its metadata. See the module
|
|
102
169
|
* doc for the contract. Memo steps are gated to EXPRESSION-BODIED memos
|
|
@@ -111,6 +178,7 @@ export function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan {
|
|
|
111
178
|
}
|
|
112
179
|
|
|
113
180
|
const available = new Set<string>(baseScope)
|
|
181
|
+
const localConsts = localConstExprsByName(metadata)
|
|
114
182
|
const steps: SsrSeedStep[] = []
|
|
115
183
|
|
|
116
184
|
for (const signal of metadata.signals) {
|
|
@@ -126,7 +194,13 @@ export function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan {
|
|
|
126
194
|
steps.push(
|
|
127
195
|
expr === ''
|
|
128
196
|
? { kind: 'opaque', name: signal.getter, origin: 'signal' }
|
|
129
|
-
: classify(
|
|
197
|
+
: classify(
|
|
198
|
+
signal.getter,
|
|
199
|
+
'signal',
|
|
200
|
+
expr,
|
|
201
|
+
resolveThroughLocalConsts(parseExpression(expr), localConsts),
|
|
202
|
+
available,
|
|
203
|
+
),
|
|
130
204
|
)
|
|
131
205
|
available.add(signal.getter)
|
|
132
206
|
}
|
|
@@ -137,7 +211,13 @@ export function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan {
|
|
|
137
211
|
steps.push(
|
|
138
212
|
expr === ''
|
|
139
213
|
? { kind: 'opaque', name: memo.name, origin: 'memo' }
|
|
140
|
-
: classify(
|
|
214
|
+
: classify(
|
|
215
|
+
memo.name,
|
|
216
|
+
'memo',
|
|
217
|
+
expr,
|
|
218
|
+
resolveThroughLocalConsts(memo.parsed ?? parseExpression(expr), localConsts),
|
|
219
|
+
available,
|
|
220
|
+
),
|
|
141
221
|
)
|
|
142
222
|
available.add(memo.name)
|
|
143
223
|
}
|