@abide/abide 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +330 -494
- package/CHANGELOG.md +106 -0
- package/README.md +166 -146
- package/package.json +3 -4
- package/src/checkAbide.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +2 -1
- package/src/lib/server/rpc/types/RpcHelper.ts +29 -22
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +3 -2
- package/src/lib/server/runtime/amendBroadcaster.ts +53 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +54 -20
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/amendFamilyEntry.ts +39 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/defineSocket.ts +11 -1
- package/src/lib/shared/AMEND_TOPIC_PREFIX.ts +9 -0
- package/src/lib/shared/SEEDS.ts +33 -0
- package/src/lib/shared/SOCKET_SEED.ts +12 -0
- package/src/lib/shared/amend.ts +48 -0
- package/src/lib/shared/amendBroadcastSlot.ts +19 -0
- package/src/lib/shared/applyAmendLocally.ts +20 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +5 -5
- package/src/lib/shared/buildSocketOverChannel.ts +9 -2
- package/src/lib/shared/cache.ts +203 -84
- package/src/lib/shared/cacheReaderSocketSlot.ts +11 -0
- package/src/lib/shared/createCacheStore.ts +8 -0
- package/src/lib/shared/createRemoteFunction.ts +4 -1
- package/src/lib/shared/encodeSeedValue.ts +24 -0
- package/src/lib/shared/jsonSchemaForType.ts +3 -16
- package/src/lib/shared/peek.ts +4 -7
- package/src/lib/shared/socketTailsSlot.ts +13 -0
- package/src/lib/shared/types/AmendApply.ts +15 -0
- package/src/lib/shared/types/CacheReaderHook.ts +11 -0
- package/src/lib/shared/types/RemoteFunction.ts +30 -19
- package/src/lib/shared/types/SocketTails.ts +13 -0
- package/src/lib/shared/types/SsrPayload.ts +6 -0
- package/src/lib/test/createTestApp.ts +6 -0
- package/src/lib/ui/amendReaderHook.ts +125 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -6
- package/src/lib/ui/compile/analyzeComponent.ts +0 -2
- package/src/lib/ui/compile/awaitSubjectExpr.ts +23 -0
- package/src/lib/ui/compile/cachedShadowProgram.ts +39 -0
- package/src/lib/ui/compile/compileComponent.ts +1 -3
- package/src/lib/ui/compile/componentOrdinals.ts +39 -0
- package/src/lib/ui/compile/desugarSignals.ts +65 -36
- package/src/lib/ui/compile/generateBuild.ts +25 -24
- package/src/lib/ui/compile/generateSSR.ts +37 -24
- package/src/lib/ui/compile/hoistableAwaits.ts +1 -11
- package/src/lib/ui/compile/interpolationClassifierForRoot.ts +8 -13
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +46 -5
- package/src/lib/ui/compile/lowerCompoundAssignment.ts +49 -0
- package/src/lib/ui/compile/lowerContext.ts +26 -6
- package/src/lib/ui/compile/lowerDocAccess.ts +19 -31
- package/src/lib/ui/compile/lowerScript.ts +1 -11
- package/src/lib/ui/compile/lowerUpdateExpression.ts +32 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -45
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +10 -15
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +1 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +3 -5
- package/src/lib/ui/compile/wrapReactionCellSources.ts +10 -6
- package/src/lib/ui/compile/wrapReactiveSeeds.ts +44 -0
- package/src/lib/ui/compile/wrapSeed.ts +28 -0
- package/src/lib/ui/computed.ts +16 -0
- package/src/lib/ui/createScope.ts +4 -3
- package/src/lib/ui/dom/anchorCursor.ts +2 -1
- package/src/lib/ui/dom/anchoredBranch.ts +14 -3
- package/src/lib/ui/dom/appendStatic.ts +11 -20
- package/src/lib/ui/dom/appendText.ts +41 -43
- package/src/lib/ui/dom/appendTextAt.ts +10 -57
- package/src/lib/ui/dom/awaitBlock.ts +31 -45
- package/src/lib/ui/dom/awaitSubject.ts +118 -0
- package/src/lib/ui/dom/claimText.ts +38 -0
- package/src/lib/ui/dom/cloneStatic.ts +2 -8
- package/src/lib/ui/dom/discardAndRebuild.ts +23 -0
- package/src/lib/ui/dom/discardBoundary.ts +5 -4
- package/src/lib/ui/dom/each.ts +3 -5
- package/src/lib/ui/dom/fillBoundary.ts +2 -1
- package/src/lib/ui/dom/hydrate.ts +5 -12
- package/src/lib/ui/dom/mountChild.ts +73 -5
- package/src/lib/ui/dom/mountSwappableRange.ts +2 -1
- package/src/lib/ui/dom/openMarker.ts +2 -4
- package/src/lib/ui/dom/outlet.ts +4 -3
- package/src/lib/ui/dom/readCell.ts +26 -11
- package/src/lib/ui/dom/skeleton.ts +2 -7
- package/src/lib/ui/dom/switchBlock.ts +2 -1
- package/src/lib/ui/dom/tryBlock.ts +27 -39
- package/src/lib/ui/dom/when.ts +6 -2
- package/src/lib/ui/linked.ts +14 -0
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +12 -16
- package/src/lib/ui/runtime/AsyncCellError.ts +2 -1
- package/src/lib/ui/runtime/BRANCH_SEGMENT.ts +24 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +4 -6
- package/src/lib/ui/runtime/DOC_SEED.ts +5 -6
- package/src/lib/ui/runtime/RENDER.ts +7 -5
- package/src/lib/ui/runtime/RESUME.ts +5 -7
- package/src/lib/ui/runtime/SEED_MARKS.ts +14 -0
- package/src/lib/ui/runtime/SuspenseSignal.ts +2 -2
- package/src/lib/ui/runtime/claimChild.ts +5 -7
- package/src/lib/ui/runtime/claimExpected.ts +2 -6
- package/src/lib/ui/runtime/claimMarker.ts +13 -0
- package/src/lib/ui/runtime/claimRun.ts +22 -0
- package/src/lib/ui/runtime/consumeSeed.ts +38 -0
- package/src/lib/ui/runtime/createAsyncCell.ts +56 -8
- package/src/lib/ui/runtime/createComputedNode.ts +1 -0
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- package/src/lib/ui/runtime/createEffectNode.ts +1 -0
- package/src/lib/ui/runtime/createSignalNode.ts +1 -0
- package/src/lib/ui/runtime/parkCursor.ts +10 -0
- package/src/lib/ui/runtime/readNode.ts +21 -1
- package/src/lib/ui/runtime/renderPath.ts +2 -2
- package/src/lib/ui/runtime/routeBareSeed.ts +28 -0
- package/src/lib/ui/runtime/runHydrationPass.ts +69 -0
- package/src/lib/ui/runtime/runNode.ts +14 -0
- package/src/lib/ui/runtime/types/Cell.ts +4 -2
- package/src/lib/ui/runtime/types/Doc.ts +3 -1
- package/src/lib/ui/runtime/types/HydrationCursor.ts +8 -0
- package/src/lib/ui/runtime/types/ReactiveNode.ts +5 -0
- package/src/lib/ui/settleAsyncCells.ts +30 -3
- package/src/lib/ui/socketProxy.ts +17 -1
- package/src/lib/ui/startClient.ts +23 -0
- package/src/lib/ui/tryEncodeResume.ts +6 -15
- package/src/lib/ui/types/Scope.ts +1 -1
- package/src/lib/ui/watch.ts +21 -16
- package/src/serverEntry.ts +15 -0
- package/src/lib/shared/patch.ts +0 -41
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +0 -19
- package/src/lib/ui/dom/mountStreamedChild.ts +0 -84
- package/src/lib/ui/dom/readCellBlocking.ts +0 -29
|
@@ -4,12 +4,13 @@ import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
|
4
4
|
import { ANCHOR } from '../runtime/RANGE_MARKER.ts'
|
|
5
5
|
import { asOutlet } from './asOutlet.ts'
|
|
6
6
|
import { awaitPlan } from './awaitPlan.ts'
|
|
7
|
+
import { awaitSubjectExpr } from './awaitSubjectExpr.ts'
|
|
7
8
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
9
|
+
import { componentOrdinals } from './componentOrdinals.ts'
|
|
8
10
|
import { composeProps } from './composeProps.ts'
|
|
9
11
|
import { eachPlan } from './eachPlan.ts'
|
|
10
12
|
import { elementPlan } from './elementPlan.ts'
|
|
11
13
|
import { groupBindParts } from './groupBindParts.ts'
|
|
12
|
-
import { hoistableChildRenders } from './hoistableChildRenders.ts'
|
|
13
14
|
import { ifPlan } from './ifPlan.ts'
|
|
14
15
|
import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
|
|
15
16
|
import { isControlFlow } from './isControlFlow.ts'
|
|
@@ -75,23 +76,12 @@ export function generateBuild(
|
|
|
75
76
|
derivedNames: ReadonlySet<string>,
|
|
76
77
|
computedNames: ReadonlySet<string>,
|
|
77
78
|
isLayout = false,
|
|
78
|
-
/* `linked` / async `computed` names, lowered to `$$readCell(name)` in template exprs
|
|
79
|
+
/* `linked` / async `computed` names, lowered to `$$readCell(name)` in template exprs — one
|
|
80
|
+
read form whose pause-vs-peek (a blocking `await` cell suspends the region; a streaming
|
|
81
|
+
cell peeks `undefined`) is decided at runtime by the cell's own `blocking` bit (ADR-0042). */
|
|
79
82
|
cellReadNames: ReadonlySet<string> = new Set(),
|
|
80
|
-
/* The subset that are BLOCKING `await` cells (ADR-0042), lowered to `$$readCellBlocking(name)`
|
|
81
|
-
on the CLIENT so a pending read suspends its render region. Server keeps `$$readCell` (the
|
|
82
|
-
SSR barrier resolves blocking cells before the template, so the read never suspends). */
|
|
83
|
-
blockingCellNames: ReadonlySet<string> = new Set(),
|
|
84
83
|
): string {
|
|
85
84
|
const nextVar = makeVarNamer()
|
|
86
|
-
/* A per-body source-order ordinal for each `<Child/>` mount site, so two same-type siblings
|
|
87
|
-
get distinct render-path segments. Combined with the ambient branch/row path a control-flow
|
|
88
|
-
ancestor pushes, this makes every mounted instance's id unique and stable (ADR render-path
|
|
89
|
-
identity). Drawn in document-order walk, matching the SSR back-end's identical walk. */
|
|
90
|
-
let childOrdinal = 0
|
|
91
|
-
/* ADR-0039: the SAME hoistable-child set the SSR back-end computes (identical inputs → identical
|
|
92
|
-
node identities), so the client emits the dual-mode `$$mountStreamedChild` for exactly the
|
|
93
|
-
children the server may stream. A non-hoistable child keeps the plain `$$mountChild`. */
|
|
94
|
-
const streamCapable = hoistableChildRenders(nodes, cellReadNames)
|
|
95
85
|
|
|
96
86
|
/* In a layout, `<slot/>` outlets are rewritten to `OUTLET_TAG` elements up front
|
|
97
87
|
(`asOutlet`) so the static-clone path carries them as ordinary structure. `asOutlet`
|
|
@@ -105,6 +95,12 @@ export function generateBuild(
|
|
|
105
95
|
position structurally (the drift the shared context exists to prevent). */
|
|
106
96
|
const { markText, elIndex, anIndex } = skeletonContext(rootNodes)
|
|
107
97
|
|
|
98
|
+
/* Each `<Child/>` mount site's render-path ordinal, from the ONE shared document-order
|
|
99
|
+
walk the SSR back-end also reads (`componentOrdinals`) — so two same-type siblings get
|
|
100
|
+
distinct, SSR-congruent segments regardless of either back-end's emission order. The
|
|
101
|
+
returned lookup throws on a miss (a structural divergence from the shared walk). */
|
|
102
|
+
const childOrdinal = componentOrdinals(rootNodes)
|
|
103
|
+
|
|
108
104
|
/* The hole's index, assigned by the shared skeletonContext walk — the sole numberer. A
|
|
109
105
|
missing entry means this back-end reached a hole the shared walk didn't number: a
|
|
110
106
|
structural divergence between the two, surfaced loudly at compile time rather than as a
|
|
@@ -121,11 +117,13 @@ export function generateBuild(
|
|
|
121
117
|
const {
|
|
122
118
|
expression: lowerExpression,
|
|
123
119
|
statement: lowerStatement,
|
|
120
|
+
script: lowerScript,
|
|
124
121
|
withNestedScripts,
|
|
125
122
|
withShadow,
|
|
123
|
+
isShadowed,
|
|
126
124
|
bindRead,
|
|
127
125
|
bindWrite,
|
|
128
|
-
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames
|
|
126
|
+
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames)
|
|
129
127
|
|
|
130
128
|
/* Maps a plan `Binding`'s classification to the client `ShadowKind`: a `reactive` value
|
|
131
129
|
derefs as a `.value` cell (`derived`), a `plain` value as the bare local (`plain`). The
|
|
@@ -285,7 +283,7 @@ export function generateBuild(
|
|
|
285
283
|
runs as a bind, in document order, so its signals are declared before the later
|
|
286
284
|
siblings that deref them (the enclosing `withNestedScripts` puts those names in
|
|
287
285
|
scope). */
|
|
288
|
-
binds.push(`${
|
|
286
|
+
binds.push(`${lowerScript(node.code)}\n`)
|
|
289
287
|
return ''
|
|
290
288
|
}
|
|
291
289
|
if (node.kind === 'snippet') {
|
|
@@ -419,7 +417,7 @@ export function generateBuild(
|
|
|
419
417
|
/* Emits code appending `node` to `parentVar`. */
|
|
420
418
|
function generateChild(node: TemplateNode, parentVar: string, before = 'null'): string {
|
|
421
419
|
if (node.kind === 'script') {
|
|
422
|
-
return `${
|
|
420
|
+
return `${lowerScript(node.code)}\n`
|
|
423
421
|
}
|
|
424
422
|
/* A `<style>` emits no DOM — its CSS is bundled and its scope attribute is
|
|
425
423
|
already stamped onto the elements it covers (see `staticHtml`/`skeletonMarkup`). */
|
|
@@ -616,11 +614,11 @@ export function generateBuild(
|
|
|
616
614
|
cell, so `<Icon>` from `{#for {icon: Icon} of …}` mounts the component the cell
|
|
617
615
|
holds, not the cell object (whose `.build` is undefined → `build is not a
|
|
618
616
|
function`). SSR emits the same lowering for congruence. */
|
|
619
|
-
/*
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return
|
|
617
|
+
/* `mountChild` is dual-mode (ADR-0039): on hydrate it probes the cursor to adopt an
|
|
618
|
+
inlined `[ … ]` range OR a streamed `abide:await` boundary, so the client build
|
|
619
|
+
needs no hoistable-child knowledge — inline-vs-stream is decided server-side per
|
|
620
|
+
render, and the adopter reads which happened off the markup. */
|
|
621
|
+
return `$$mountChild(${parentVar}, ${lowerExpression(node.name)}, ${propsArg(node)}, ${before}, ${childOrdinal(node)});\n`
|
|
624
622
|
}
|
|
625
623
|
|
|
626
624
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
@@ -649,8 +647,11 @@ export function generateBuild(
|
|
|
649
647
|
const pendingThunk = hasRenderableContent(plan.pending)
|
|
650
648
|
? branchThunk(plan.pending)
|
|
651
649
|
: 'undefined'
|
|
650
|
+
/* The subject lowers through the ONE shared ADR-0047 decision site (`awaitSubjectExpr`),
|
|
651
|
+
so the bare-cell classification can't drift from the SSR back-end's. */
|
|
652
|
+
const subject = awaitSubjectExpr(node.promise, cellReadNames, isShadowed, lowerExpression)
|
|
652
653
|
return (
|
|
653
|
-
`$$awaitBlock(${parentVar}, $$nextBlockId(), () => (${
|
|
654
|
+
`$$awaitBlock(${parentVar}, $$nextBlockId(), () => (${subject}), ` +
|
|
654
655
|
`${pendingThunk}, ` +
|
|
655
656
|
`${thenThunk}, ` +
|
|
656
657
|
`${catchThunk}, ${before});\n`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
2
2
|
import { escapeRegex } from '../../shared/escapeRegex.ts'
|
|
3
|
+
import { caseSegment, ELSE_SEGMENT, THEN_SEGMENT } from '../runtime/BRANCH_SEGMENT.ts'
|
|
3
4
|
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
4
5
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
5
6
|
import {
|
|
@@ -9,6 +10,8 @@ import {
|
|
|
9
10
|
} from '../runtime/RANGE_MARKER.ts'
|
|
10
11
|
import { asOutlet } from './asOutlet.ts'
|
|
11
12
|
import { awaitPlan } from './awaitPlan.ts'
|
|
13
|
+
import { awaitSubjectExpr } from './awaitSubjectExpr.ts'
|
|
14
|
+
import { componentOrdinals } from './componentOrdinals.ts'
|
|
12
15
|
import { composeProps } from './composeProps.ts'
|
|
13
16
|
import { eachPlan } from './eachPlan.ts'
|
|
14
17
|
import { elementPlan } from './elementPlan.ts'
|
|
@@ -74,10 +77,6 @@ export function generateSSR(
|
|
|
74
77
|
/* Unique temp var names (child render results); runtime block ids are
|
|
75
78
|
allocated separately at runtime via `$ctx.next++`. */
|
|
76
79
|
const nextVar = makeVarNamer()
|
|
77
|
-
/* A per-body source-order ordinal for each `<Child/>` render site — the render-path segment the
|
|
78
|
-
child roots under, matching the client `mountChild`'s `childOrdinal` (`generateBuild`) drawn
|
|
79
|
-
in the identical document-order walk, so both compose the same cell scope ids. */
|
|
80
|
-
let childOrdinal = 0
|
|
81
80
|
|
|
82
81
|
/* The enclosing `<select bind:value>`s, innermost last: each carries the JS var holding
|
|
83
82
|
its bound value and whether it's a `multiple` (array) select, so an `<option>` rendered
|
|
@@ -88,9 +87,10 @@ export function generateSSR(
|
|
|
88
87
|
/* The shared signal→`model` lowering + branch-scoped nested-script deref scope. */
|
|
89
88
|
const {
|
|
90
89
|
expression: lowerExpression,
|
|
91
|
-
|
|
90
|
+
script,
|
|
92
91
|
withNestedScripts,
|
|
93
92
|
withShadow,
|
|
93
|
+
isShadowed,
|
|
94
94
|
bindRead,
|
|
95
95
|
bindWrite,
|
|
96
96
|
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames)
|
|
@@ -102,7 +102,7 @@ export function generateSSR(
|
|
|
102
102
|
|
|
103
103
|
/* A scoped-script body for SSR: the shared lowering, then strip effects
|
|
104
104
|
(client-only lifecycle that emits no HTML) — the one SSR-side asymmetry. */
|
|
105
|
-
const lowerScript = (code: string): string => stripEffects(
|
|
105
|
+
const lowerScript = (code: string): string => stripEffects(script(code))
|
|
106
106
|
|
|
107
107
|
function push(target: string, literal: string): string {
|
|
108
108
|
return `${target}.push(${JSON.stringify(literal)});\n`
|
|
@@ -159,6 +159,13 @@ export function generateSSR(
|
|
|
159
159
|
through the generic element path exactly as the client clones it. */
|
|
160
160
|
const rootNodes = isLayout ? nodes.map(asOutlet) : nodes
|
|
161
161
|
|
|
162
|
+
/* Each `<Child/>` render site's render-path ordinal, from the ONE shared document-order
|
|
163
|
+
walk the client back-end also reads (`componentOrdinals`) — so both compose the same
|
|
164
|
+
cell scope ids and streamed-boundary ids regardless of either back-end's emission
|
|
165
|
+
order. The returned lookup throws on a miss (a structural divergence from the shared
|
|
166
|
+
walk), surfaced loudly at compile time. */
|
|
167
|
+
const childOrdinal = componentOrdinals(rootNodes)
|
|
168
|
+
|
|
162
169
|
/* ADR-0034: the await blocks whose promise-start hoists to the synchronous render prefix so
|
|
163
170
|
independent flights overlap instead of serializing. Server-only — this rewires only the SSR
|
|
164
171
|
promise SOURCE (a blocking `await $flightN`, a streaming `promise: () => $flightN`); the
|
|
@@ -362,10 +369,10 @@ export function generateSSR(
|
|
|
362
369
|
then the `$$readCell` truthy test — so sync and async branches interleave and each
|
|
363
370
|
resolves at render time, mirroring the client `switchBlock` cond-chain. */
|
|
364
371
|
const plan = ifPlan(node)
|
|
365
|
-
/* The client build lowers a simple `if`/`else` through `when`
|
|
366
|
-
`
|
|
367
|
-
|
|
368
|
-
`<Child/>`'s scope id composes identically. */
|
|
372
|
+
/* The client build lowers a simple `if`/`else` through `when` and an `if` with any
|
|
373
|
+
`elseif` through `switchBlock` over `[then, ...branches]`. Segments come from the
|
|
374
|
+
SAME alphabet those runtimes key their branches with (`BRANCH_SEGMENT`), so a
|
|
375
|
+
nested `<Child/>`'s scope id composes identically — no literal to drift. */
|
|
369
376
|
const clauses: string[] = []
|
|
370
377
|
const conditional = (
|
|
371
378
|
condition: string,
|
|
@@ -385,12 +392,12 @@ export function generateSSR(
|
|
|
385
392
|
)
|
|
386
393
|
}
|
|
387
394
|
}
|
|
388
|
-
/* `then` is the `when`
|
|
395
|
+
/* `then` is the `when` then-branch, or the `switchBlock` case at index 0. */
|
|
389
396
|
conditional(
|
|
390
397
|
node.condition,
|
|
391
398
|
node.asyncSubject,
|
|
392
399
|
plan.thenChildren,
|
|
393
|
-
JSON.stringify(plan.hasElseif ?
|
|
400
|
+
JSON.stringify(plan.hasElseif ? caseSegment(0) : THEN_SEGMENT),
|
|
394
401
|
)
|
|
395
402
|
for (let index = 0; index < plan.branches.length; index += 1) {
|
|
396
403
|
const branch = plan.branches[index] as Extract<TemplateNode, { kind: 'case' }>
|
|
@@ -401,7 +408,7 @@ export function generateSSR(
|
|
|
401
408
|
branch.condition,
|
|
402
409
|
branch.asyncSubject,
|
|
403
410
|
branch.children,
|
|
404
|
-
JSON.stringify(
|
|
411
|
+
JSON.stringify(caseSegment(index + 1)),
|
|
405
412
|
)
|
|
406
413
|
}
|
|
407
414
|
}
|
|
@@ -409,11 +416,11 @@ export function generateSSR(
|
|
|
409
416
|
.map((clause, index) => `${index === 0 ? 'if' : ' else if'} ${clause}`)
|
|
410
417
|
.join('')
|
|
411
418
|
if (plan.elseBranch !== undefined) {
|
|
412
|
-
/* `else` is the `when`
|
|
419
|
+
/* `else` is the `when` else-branch, or (for a cond-chain) the `switchBlock`
|
|
413
420
|
default at its own `[then, ...branches]` index. */
|
|
414
421
|
const elseSegment = plan.hasElseif
|
|
415
|
-
?
|
|
416
|
-
:
|
|
422
|
+
? caseSegment(plan.branches.indexOf(plan.elseBranch) + 1)
|
|
423
|
+
: ELSE_SEGMENT
|
|
417
424
|
code += ` else {\n${withPathBranch(JSON.stringify(elseSegment), plan.elseBranch.children, target)}}`
|
|
418
425
|
}
|
|
419
426
|
return `${anchor}${openRange(target)}${code}\n${closeRange(target)}`
|
|
@@ -436,12 +443,12 @@ export function generateSSR(
|
|
|
436
443
|
for (let index = 0; index < plan.cases.length; index += 1) {
|
|
437
444
|
const branch = plan.cases[index] as Extract<TemplateNode, { kind: 'case' }>
|
|
438
445
|
if (branch.match !== undefined) {
|
|
439
|
-
code += `${started ? 'else ' : ''}if ($s === (${lowerExpression(branch.match)})) {\n${withPathBranch(JSON.stringify(
|
|
446
|
+
code += `${started ? 'else ' : ''}if ($s === (${lowerExpression(branch.match)})) {\n${withPathBranch(JSON.stringify(caseSegment(index)), branch.children, target)}}\n`
|
|
440
447
|
started = true
|
|
441
448
|
}
|
|
442
449
|
}
|
|
443
450
|
if (plan.fallback !== undefined) {
|
|
444
|
-
const fallbackSegment =
|
|
451
|
+
const fallbackSegment = caseSegment(plan.cases.indexOf(plan.fallback))
|
|
445
452
|
code += `${started ? 'else ' : ''}{\n${withPathBranch(JSON.stringify(fallbackSegment), plan.fallback.children, target)}}\n`
|
|
446
453
|
}
|
|
447
454
|
if (node.asyncSubject === true) {
|
|
@@ -583,10 +590,10 @@ export function generateSSR(
|
|
|
583
590
|
/* The tag lowers like any reference (see generateBuild): a static import is left bare, a
|
|
584
591
|
reactive/loop/await binding derefs — SSR registers such a binding as `plain`, so it
|
|
585
592
|
reads the bare local holding the resolved component, keeping SSR and client congruent.
|
|
586
|
-
Root the child's render-path at this mount site's
|
|
593
|
+
Root the child's render-path at this mount site's shared-walk ordinal — the same
|
|
587
594
|
segment the client's `mountChild` pushes — so the child's cells and block ids (ADR-0037)
|
|
588
595
|
get an id matching the client's. `$$withPath` sets the path across the child's awaits. */
|
|
589
|
-
const ordinal = childOrdinal
|
|
596
|
+
const ordinal = childOrdinal(node)
|
|
590
597
|
const renderExpr = `$$withPath(${ordinal}, () => ${lowerExpression(node.name)}.render(${propsExpr}, $ctx))`
|
|
591
598
|
if (hoistableChildSet.has(node)) {
|
|
592
599
|
/* Hoistable (ADR-0037/0039): start the render as an isolated flight in the prefix, and
|
|
@@ -595,7 +602,7 @@ export function generateSSR(
|
|
|
595
602
|
flight already settled (byte-identical to the old inline await), or a streaming
|
|
596
603
|
`abide:await:CHILDPATH` boundary + html-only await if it is still pending. The
|
|
597
604
|
boundary id is the child's render-path (`$$renderPath(ordinal)`), congruent with the
|
|
598
|
-
client's `
|
|
605
|
+
client's dual-mode `mountChild`. */
|
|
599
606
|
const flightName = `$cf${childFlightCounter++}`
|
|
600
607
|
childFlightDecls.push(
|
|
601
608
|
`const ${flightName} = $$flight(() => $$isolateCellBarrier(() => ${renderExpr}));`,
|
|
@@ -787,6 +794,12 @@ export function generateSSR(
|
|
|
787
794
|
: generateStreamingAwait(node, target)
|
|
788
795
|
}
|
|
789
796
|
|
|
797
|
+
/* The lowered `{#await X}` subject (ADR-0047) — the ONE shared decision site, so the
|
|
798
|
+
bare-cell classification can't drift from the client back-end's. Used by the
|
|
799
|
+
blocking, streaming, and flight-hoist emit sites. */
|
|
800
|
+
const subjectExprFor = (promiseCode: string): string =>
|
|
801
|
+
awaitSubjectExpr(promiseCode, cellReadNames, isShadowed, lowerExpression)
|
|
802
|
+
|
|
790
803
|
/*
|
|
791
804
|
A blocking await — `await`ed at its structural position in the async render pass and
|
|
792
805
|
its resolved branch rendered INLINE into `target`, between the boundary markers. This
|
|
@@ -819,7 +832,7 @@ export function generateSSR(
|
|
|
819
832
|
let code = `const ${id} = $$blockId($ctx);\n`
|
|
820
833
|
code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
|
|
821
834
|
code += `try {\n`
|
|
822
|
-
code += `const ${resolved} = await (${hoistedPromise ??
|
|
835
|
+
code += `const ${resolved} = await (${hoistedPromise ?? subjectExprFor(node.promise)});\n`
|
|
823
836
|
code += `{\n`
|
|
824
837
|
code += `const ${plan.resolvedAs} = ${resolved};\n`
|
|
825
838
|
code += withBindings(withShadow, plan.resolvedBindings, ssrBindingKind, () =>
|
|
@@ -891,7 +904,7 @@ export function generateSSR(
|
|
|
891
904
|
const hoistedPromise = flightNameByNode.get(node)
|
|
892
905
|
code +=
|
|
893
906
|
`$awaits.push({ id: ${id}, ` +
|
|
894
|
-
`promise: () => (${hoistedPromise ??
|
|
907
|
+
`promise: () => (${hoistedPromise ?? subjectExprFor(node.promise)}), ` +
|
|
895
908
|
`then: ${settled(plan.resolvedAs, plan.resolvedBindings, plan.resolvedChildren)}, ` +
|
|
896
909
|
`${catchProp}});\n`
|
|
897
910
|
return code
|
|
@@ -939,7 +952,7 @@ export function generateSSR(
|
|
|
939
952
|
const body = generateInto(rootNodes, '$out')
|
|
940
953
|
let flightDecls = ''
|
|
941
954
|
for (const [node, name] of flightNameByNode) {
|
|
942
|
-
flightDecls += `const ${name} = $$flight(() => (${
|
|
955
|
+
flightDecls += `const ${name} = $$flight(() => (${subjectExprFor(node.promise)}));\n`
|
|
943
956
|
}
|
|
944
957
|
/* Child-render flights emitted during the body walk above (ADR-0037 Phase 2) — appended after
|
|
945
958
|
the await flights; both are independent prefix promise-starts, so order between them is free. */
|
|
@@ -78,7 +78,7 @@ function walkNode(
|
|
|
78
78
|
if (
|
|
79
79
|
!conditional &&
|
|
80
80
|
!multiRow &&
|
|
81
|
-
|
|
81
|
+
expressionIsPrefixEvaluable(node.promise, binders, cellReadNames)
|
|
82
82
|
) {
|
|
83
83
|
flights.push({ node, name: `$flight${counter.next++}` })
|
|
84
84
|
}
|
|
@@ -152,16 +152,6 @@ function walkNode(
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/* True when EVERY referenced identifier of `promise` is prefix-evaluable: none is a template-local
|
|
156
|
-
binder and none is an async-cell name. Delegates to the shared `expressionIsPrefixEvaluable`. */
|
|
157
|
-
function promiseIsPrefixEvaluable(
|
|
158
|
-
promise: string,
|
|
159
|
-
binders: ReadonlySet<string>,
|
|
160
|
-
cellReadNames: ReadonlySet<string>,
|
|
161
|
-
): boolean {
|
|
162
|
-
return expressionIsPrefixEvaluable(promise, binders, cellReadNames)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
155
|
/* A single-element array literal like `[remountKey]` — the only `{#for}` source that renders its
|
|
166
156
|
body exactly once, so a hoisted flight inside it maps to one row (no shared-flight hazard). */
|
|
167
157
|
function isSingleElementLiteral(items: string): boolean {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cachedShadowProgram } from './cachedShadowProgram.ts'
|
|
2
|
+
import type { ShadowProgram } from './createShadowProgram.ts'
|
|
2
3
|
import { shadowInterpolationClassifier } from './shadowInterpolationClassifier.ts'
|
|
3
4
|
import type { InterpolationClassifier } from './types/InterpolationClassifier.ts'
|
|
4
5
|
|
|
@@ -12,24 +13,18 @@ The incremental `createShadowLanguageService` does not expose the checker, shado
|
|
|
12
13
|
`ts.SourceFile`, or per-file mappings publicly, so v1 uses the one-shot
|
|
13
14
|
`createShadowProgram` (reused per root) rather than the LS overlay path.
|
|
14
15
|
|
|
15
|
-
FAIL-OPEN throughout: if the program can't be built
|
|
16
|
-
classifier is absent (→ today's
|
|
17
|
-
|
|
18
|
-
`'sync'`. A type-resolution
|
|
16
|
+
FAIL-OPEN throughout: if the program can't be built (warned once per root by
|
|
17
|
+
`cachedShadowProgram`), or the file has no shadow, the classifier is absent (→ today's
|
|
18
|
+
plain-value binding); and the returned closure wraps its whole body so ANY throw
|
|
19
|
+
(resolution failure, missing node, checker hiccup) returns `'sync'`. A type-resolution
|
|
20
|
+
problem degrades to today's behavior, never breaks the build.
|
|
19
21
|
*/
|
|
20
22
|
export function interpolationClassifierForRoot(
|
|
21
23
|
cache: Map<string, ShadowProgram | undefined>,
|
|
22
24
|
root: string,
|
|
23
25
|
abidePath: string,
|
|
24
26
|
): InterpolationClassifier | undefined {
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
cache.set(root, createShadowProgram(root))
|
|
28
|
-
} catch {
|
|
29
|
-
cache.set(root, undefined)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const shadowProgram = cache.get(root)
|
|
27
|
+
const shadowProgram = cachedShadowProgram(cache, root)
|
|
33
28
|
if (shadowProgram === undefined) {
|
|
34
29
|
return undefined
|
|
35
30
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { classifyInterpolationType } from './classifyInterpolationType.ts'
|
|
3
3
|
|
|
4
|
-
/* The
|
|
4
|
+
/* The three TypeScript diagnostics a bare async read (ADR-0032) raises spuriously in a template.
|
|
5
5
|
2339 — a property "missing" on the un-awaited `Promise`/`AsyncIterable`; 2801 — a condition the
|
|
6
|
-
raw promise makes "always defined"
|
|
7
|
-
|
|
6
|
+
raw promise makes "always defined"; 2488 — a promise source "not iterable" in a plain `{#for}`.
|
|
7
|
+
All are correct against the raw type but wrong against the runtime PEEK (the resolved value,
|
|
8
|
+
`undefined` while pending), which the shadow can't see. */
|
|
8
9
|
const PROPERTY_DOES_NOT_EXIST = 2339
|
|
9
10
|
const CONDITION_ALWAYS_DEFINED = 2801
|
|
11
|
+
const SOURCE_NOT_ITERABLE = 2488
|
|
10
12
|
|
|
11
13
|
/*
|
|
12
14
|
Whether a shadow diagnostic is one the ADR-0032 bare-async-read peek makes spurious. The runtime
|
|
@@ -20,11 +22,16 @@ caller can drop them:
|
|
|
20
22
|
property EXISTS on the awaited (promise) or frame (iterable) type — the peek's real member.
|
|
21
23
|
- 2801 (always-defined condition): the flagged subject itself classifies as a promise/iterable —
|
|
22
24
|
the peek is genuinely nullable (pending → `undefined`), so it is not always-defined.
|
|
25
|
+
- 2488 (source not iterable): a plain `{#for}` source classifies as a promise whose AWAITED type
|
|
26
|
+
is itself iterable — the runtime lifts it to a peek-cell reading the resolved collection (empty
|
|
27
|
+
while pending), so iterating it is valid (matches `typeDirectedInterpolation`'s peek-cell lift).
|
|
23
28
|
|
|
24
|
-
Deliberately narrow so it never hides a real mistake: only these
|
|
29
|
+
Deliberately narrow so it never hides a real mistake: only these three codes; 2339 only via `?.`
|
|
25
30
|
(a bare `.name` on a promise stays an error, nudging toward the `?.` the peek needs) and only when
|
|
26
31
|
the property resolves on the awaited/frame type (a typo `?.namex` stays); 2801 only when the
|
|
27
|
-
subject is actually async (a bare `{#if someFn}` — a forgotten call — stays)
|
|
32
|
+
subject is actually async (a bare `{#if someFn}` — a forgotten call — stays); 2488 only for a
|
|
33
|
+
PROMISE whose awaited type is iterable (a raw `AsyncIterable` source stays the sanctioned D4a error,
|
|
34
|
+
and a `Promise<number>` source stays a real mistake the peek can't rescue). Callers additionally
|
|
28
35
|
gate on the diagnostic mapping into the template region, so `<script>` code (where a forgotten
|
|
29
36
|
`await` must surface) is never touched.
|
|
30
37
|
*/
|
|
@@ -41,6 +48,40 @@ export function isSpuriousAsyncReadDiagnostic(
|
|
|
41
48
|
if (code === CONDITION_ALWAYS_DEFINED) {
|
|
42
49
|
return isAsyncSubject(shadowFile, checker, start, length)
|
|
43
50
|
}
|
|
51
|
+
if (code === SOURCE_NOT_ITERABLE) {
|
|
52
|
+
return isResolvedIterableAsyncSource(shadowFile, checker, start, length)
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* A 2488 on a plain `{#for}` source whose expression classifies as a PROMISE and whose awaited
|
|
58
|
+
type is itself iterable — the ADR-0032 peek lifts it to the resolved collection, not an error.
|
|
59
|
+
Excludes a raw `AsyncIterable` (kind `asyncIterable`, the D4a error) and a promise of a
|
|
60
|
+
non-iterable (the peek would still not iterate). */
|
|
61
|
+
function isResolvedIterableAsyncSource(
|
|
62
|
+
shadowFile: ts.SourceFile,
|
|
63
|
+
checker: ts.TypeChecker,
|
|
64
|
+
start: number,
|
|
65
|
+
length: number,
|
|
66
|
+
): boolean {
|
|
67
|
+
const source = deepestNodeAt(shadowFile, start, length)
|
|
68
|
+
const sourceType = checker.getTypeAtLocation(source)
|
|
69
|
+
if (classifyInterpolationType(sourceType, source, checker) !== 'promise') {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
const awaited = checker.getAwaitedType(sourceType)
|
|
73
|
+
return awaited !== undefined && hasSyncIterator(awaited, checker)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Whether a type is synchronously iterable — an array/tuple or anything carrying the well-known
|
|
77
|
+
`Symbol.iterator` member (mangled to `__@iterator@<id>`, matched by prefix like
|
|
78
|
+
`classifyInterpolationType`'s async twin). */
|
|
79
|
+
function hasSyncIterator(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
80
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
81
|
+
if ((property.escapedName as string).startsWith('__@iterator')) {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
44
85
|
return false
|
|
45
86
|
}
|
|
46
87
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/* Arithmetic compound assignments → their plain binary counterpart, lowered to a
|
|
4
|
+
read-combine-write (`x += y` → `write(read + y)`). */
|
|
5
|
+
const ARITHMETIC_COMPOUND_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
6
|
+
[ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken],
|
|
7
|
+
[ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken],
|
|
8
|
+
[ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken],
|
|
9
|
+
[ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken],
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
/* Logical compound assignments SHORT-CIRCUIT: the write fires only when the guard
|
|
13
|
+
passes (`x ??= v` is `x ?? (x = v)`), so they lower to `read <op> write(right)`, NOT
|
|
14
|
+
an unconditional write — matching JS, so a non-nullish/truthy/falsy guard does no
|
|
15
|
+
needless patch or cell reseed. */
|
|
16
|
+
const LOGICAL_ASSIGNMENT_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
17
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
18
|
+
[ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken],
|
|
19
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken],
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Lowers an assignment (`=`, arithmetic `+=`, or logical `??=`/`||=`/`&&=`) to a reactive
|
|
24
|
+
write, shared by the `$$model` doc path (lowerDocAccess) and the `$$writeCell` linked-cell
|
|
25
|
+
path (renameSignalRefs) so the two lowerings can't drift. `makeRead` builds the target's
|
|
26
|
+
read expression (only called for compound operators); `makeWrite` wraps a value in the
|
|
27
|
+
write call. Returns undefined for an operator this doesn't handle so the caller can leave
|
|
28
|
+
the node untouched. The write must evaluate to the written value (see `Cell.set` /
|
|
29
|
+
`Doc.replace`) for the logical short-circuit to yield the right result.
|
|
30
|
+
*/
|
|
31
|
+
export function lowerCompoundAssignment(
|
|
32
|
+
operator: ts.SyntaxKind,
|
|
33
|
+
makeRead: () => ts.Expression,
|
|
34
|
+
right: ts.Expression,
|
|
35
|
+
makeWrite: (value: ts.Expression) => ts.Expression,
|
|
36
|
+
): ts.Expression | undefined {
|
|
37
|
+
if (operator === ts.SyntaxKind.EqualsToken) {
|
|
38
|
+
return makeWrite(right)
|
|
39
|
+
}
|
|
40
|
+
const arithmetic = ARITHMETIC_COMPOUND_OPERATORS.get(operator)
|
|
41
|
+
if (arithmetic !== undefined) {
|
|
42
|
+
return makeWrite(ts.factory.createBinaryExpression(makeRead(), arithmetic, right))
|
|
43
|
+
}
|
|
44
|
+
const logical = LOGICAL_ASSIGNMENT_OPERATORS.get(operator)
|
|
45
|
+
if (logical !== undefined) {
|
|
46
|
+
return ts.factory.createBinaryExpression(makeRead(), logical, makeWrite(right))
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
@@ -6,6 +6,7 @@ import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
|
6
6
|
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
7
7
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
8
8
|
import { unwrapParens } from './unwrapParens.ts'
|
|
9
|
+
import { wrapReactiveSeedsTransformer } from './wrapReactiveSeeds.ts'
|
|
9
10
|
|
|
10
11
|
/* Compiled once — strips the trailing `;` off every lowered expression. */
|
|
11
12
|
const TRAILING_SEMICOLON = /;$/
|
|
@@ -25,11 +26,10 @@ export function lowerContext(
|
|
|
25
26
|
stateNames: ReadonlySet<string>,
|
|
26
27
|
derivedNames: ReadonlySet<string>,
|
|
27
28
|
computedNames: ReadonlySet<string> = new Set(),
|
|
28
|
-
/* `linked` / async `computed` names, read through `$$readCell(name)
|
|
29
|
+
/* `linked` / async `computed` names, read through `$$readCell(name)` — one read form
|
|
30
|
+
whose pause-vs-peek is decided at runtime by the cell's own `blocking` bit (ADR-0042),
|
|
31
|
+
so both back-ends lower a cell read identically with nothing site-specific to thread. */
|
|
29
32
|
cellReadNames: ReadonlySet<string> = new Set(),
|
|
30
|
-
/* The subset that are BLOCKING `await` cells (ADR-0042), read through `$$readCellBlocking(name)`
|
|
31
|
-
(suspend-on-pending). Passed only by the CLIENT back-end; SSR leaves it empty. */
|
|
32
|
-
blockingCellNames: ReadonlySet<string> = new Set(),
|
|
33
33
|
) {
|
|
34
34
|
/* The typed branch-local shadow stack: one auto-popping value owning both kinds.
|
|
35
35
|
`derived` names deref to `.value` like a `computed` (block value params the client
|
|
@@ -44,9 +44,13 @@ export function lowerContext(
|
|
|
44
44
|
one tree — the two string passes would each parse + reprint. `localDerived` is
|
|
45
45
|
snapshotted per call (as the transformer's block-local shadow set) so a binding
|
|
46
46
|
pushed mid-compile is honoured AND shadows a same-named component signal. */
|
|
47
|
-
function lowerOnce(
|
|
47
|
+
function lowerOnce(
|
|
48
|
+
code: string,
|
|
49
|
+
leadingTransformers: ts.TransformerFactory<ts.SourceFile>[] = [],
|
|
50
|
+
): string {
|
|
48
51
|
const source = ts.createSourceFile('expr.ts', code, ts.ScriptTarget.Latest, true)
|
|
49
52
|
const result = ts.transform(source, [
|
|
53
|
+
...leadingTransformers,
|
|
50
54
|
signalRefsTransformer(
|
|
51
55
|
stateNames,
|
|
52
56
|
derivedNames,
|
|
@@ -54,7 +58,6 @@ export function lowerContext(
|
|
|
54
58
|
new Set(scope.names('derived')),
|
|
55
59
|
new Set(scope.names('plain')),
|
|
56
60
|
cellReadNames,
|
|
57
|
-
blockingCellNames,
|
|
58
61
|
),
|
|
59
62
|
docAccessTransformer('$$model'),
|
|
60
63
|
])
|
|
@@ -75,6 +78,16 @@ export function lowerContext(
|
|
|
75
78
|
return lowerOnce(code)
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
/* A nested branch `<script>` body. Same lowering as `statement`, but first normalises its
|
|
82
|
+
literal `state.computed`/`state.linked` seeds into thunks (`wrapReactiveSeeds`) so a bare-
|
|
83
|
+
value seed (`state.computed(a * 2)`) matches the top-level desugar rather than reaching the
|
|
84
|
+
runtime primitive un-thunked (which would call the value as the compute function and crash).
|
|
85
|
+
The wrap runs BEFORE the reference rename so it wraps the raw seed and the renamed refs land
|
|
86
|
+
inside the thunk. */
|
|
87
|
+
function script(code: string): string {
|
|
88
|
+
return lowerOnce(code, [wrapReactiveSeedsTransformer()])
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
/* A two-way bind target is either an LVALUE (`count`, `model.lines[i]`) — reads as
|
|
79
92
|
itself, writes by assignment — or an ACCESSOR object from `bind:value={{ get, set }}`
|
|
80
93
|
— reads via `.get()`, writes via `.set(v)`. The accessor is the only way to bind a
|
|
@@ -136,11 +149,18 @@ export function lowerContext(
|
|
|
136
149
|
return {
|
|
137
150
|
expression,
|
|
138
151
|
statement,
|
|
152
|
+
script,
|
|
139
153
|
withNestedScripts,
|
|
140
154
|
/* The raw branch-local shadow registration both back-ends drive `withBindings`
|
|
141
155
|
through: a block's bindings flow to a `ShadowKind` only via that one shared loop,
|
|
142
156
|
never through a per-block `scope.withShadow` call here. */
|
|
143
157
|
withShadow: scope.withShadow,
|
|
158
|
+
/* Whether `name` is currently shadowed by a block binding (either kind) at this point
|
|
159
|
+
of the walk — the live query classification sites (e.g. `awaitSubjectExpr`) consult
|
|
160
|
+
so a binder that shadows a component cell name is lowered as the local it is, never
|
|
161
|
+
wrapped as the cell it hides. */
|
|
162
|
+
isShadowed: (name: string): boolean =>
|
|
163
|
+
scope.names('plain').has(name) || scope.names('derived').has(name),
|
|
144
164
|
bindRead,
|
|
145
165
|
bindWrite,
|
|
146
166
|
}
|