@abide/abide 0.54.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 -498
- package/CHANGELOG.md +49 -0
- package/README.md +166 -146
- package/package.json +2 -3
- 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/createUiPageRenderer.ts +34 -26
- package/src/lib/shared/SEEDS.ts +33 -0
- package/src/lib/shared/SOCKET_SEED.ts +4 -5
- package/src/lib/shared/cache.ts +169 -75
- 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 +3 -6
- package/src/lib/ui/amendReaderHook.ts +1 -1
- 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 +2 -1
- package/src/lib/ui/compile/compileComponent.ts +1 -10
- package/src/lib/ui/compile/componentOrdinals.ts +39 -0
- package/src/lib/ui/compile/desugarSignals.ts +8 -44
- 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/isSpuriousAsyncReadDiagnostic.ts +46 -5
- package/src/lib/ui/compile/lowerContext.ts +26 -6
- package/src/lib/ui/compile/lowerScript.ts +1 -11
- package/src/lib/ui/compile/renameSignalRefs.ts +7 -21
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +1 -1
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +1 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +3 -5
- 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 +30 -44
- 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 +26 -38
- 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 -24
- 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/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/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/startClient.ts +0 -7
- package/src/lib/ui/tryEncodeResume.ts +6 -15
- package/src/lib/ui/watch.ts +3 -11
- package/src/lib/ui/dom/mountStreamedChild.ts +0 -84
- package/src/lib/ui/dom/readCellBlocking.ts +0 -29
- package/src/lib/ui/runtime/restoreWarmSeeds.ts +0 -20
- package/src/lib/ui/runtime/warmSeedBackup.ts +0 -10
|
@@ -120,7 +120,6 @@ export function analyzeComponent(
|
|
|
120
120
|
derivedNames,
|
|
121
121
|
computedNames,
|
|
122
122
|
cellReadNames,
|
|
123
|
-
blockingCellNames: allBlockingCellNames,
|
|
124
123
|
droppedReactiveImports,
|
|
125
124
|
} = lowerScript(
|
|
126
125
|
fullScriptBody,
|
|
@@ -143,7 +142,6 @@ export function analyzeComponent(
|
|
|
143
142
|
derivedNames,
|
|
144
143
|
computedNames,
|
|
145
144
|
cellReadNames,
|
|
146
|
-
blockingCellNames: allBlockingCellNames,
|
|
147
145
|
nodes,
|
|
148
146
|
styles,
|
|
149
147
|
/* Hydration adopts every block in place — including `await`, which resumes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The lowered `{#await X}` subject, normalised so a cell subject AWAITS its resolution
|
|
3
|
+
(ADR-0047): a BARE async-cell reference is passed RAW (not peeked to `$$readCell(cell)`)
|
|
4
|
+
and wrapped in `$$awaitSubject(...)`, which resolves a cell to a promise-of-its-value —
|
|
5
|
+
so `{#await rates}` (a cell) shows pending then the value; any other subject lowers
|
|
6
|
+
unchanged, so `{#await getFoo()}` (a plain promise) is byte-identical. A name currently
|
|
7
|
+
SHADOWED by a block binding (`{#for rates of …}` / `{:then rates}` over a component cell
|
|
8
|
+
`rates`) is the binder's local, not the cell — it lowers through `lowerExpression` like any
|
|
9
|
+
other subject, matching how every other reference site honours the shadow stack. ONE
|
|
10
|
+
decision site shared by both back-ends (the client `awaitBlock` emit and SSR's blocking /
|
|
11
|
+
streaming / flight-hoist emits), so the bare-cell classification can't drift between them.
|
|
12
|
+
*/
|
|
13
|
+
export function awaitSubjectExpr(
|
|
14
|
+
promiseCode: string,
|
|
15
|
+
cellReadNames: ReadonlySet<string>,
|
|
16
|
+
isShadowed: (name: string) => boolean,
|
|
17
|
+
lowerExpression: (code: string) => string,
|
|
18
|
+
): string {
|
|
19
|
+
const bare = promiseCode.trim()
|
|
20
|
+
return cellReadNames.has(bare) && !isShadowed(bare)
|
|
21
|
+
? `$$awaitSubject(${bare})`
|
|
22
|
+
: lowerExpression(promiseCode)
|
|
23
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
1
2
|
import { createShadowProgram, type ShadowProgram } from './createShadowProgram.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -26,7 +27,7 @@ export function cachedShadowProgram(
|
|
|
26
27
|
cache.set(root, build(root))
|
|
27
28
|
} catch (error) {
|
|
28
29
|
cache.set(root, undefined)
|
|
29
|
-
const detail =
|
|
30
|
+
const detail = messageFromError(error)
|
|
30
31
|
console.warn(
|
|
31
32
|
`[abide] couldn't build the type program for ${root}: ${detail}\n` +
|
|
32
33
|
' type-directed async detection is disabled for this project — a bare promise/stream\n' +
|
|
@@ -29,15 +29,7 @@ export function compileComponent(
|
|
|
29
29
|
a direct caller (tests) omits it and the front-end runs here — threading `classify` and
|
|
30
30
|
`seedClassify` so type-directed interpolation + cell lowering happen on this path too. */
|
|
31
31
|
const resolved = analyzed ?? analyzeComponent(source, scopeSeed, classify, seedClassify)
|
|
32
|
-
const {
|
|
33
|
-
script,
|
|
34
|
-
stateNames,
|
|
35
|
-
derivedNames,
|
|
36
|
-
computedNames,
|
|
37
|
-
cellReadNames,
|
|
38
|
-
blockingCellNames,
|
|
39
|
-
nodes,
|
|
40
|
-
} = resolved
|
|
32
|
+
const { script, stateNames, derivedNames, computedNames, cellReadNames, nodes } = resolved
|
|
41
33
|
const build = generateBuild(
|
|
42
34
|
nodes,
|
|
43
35
|
'host',
|
|
@@ -46,7 +38,6 @@ export function compileComponent(
|
|
|
46
38
|
computedNames,
|
|
47
39
|
isLayout,
|
|
48
40
|
cellReadNames,
|
|
49
|
-
blockingCellNames,
|
|
50
41
|
)
|
|
51
42
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
52
43
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Document-order (pre-order) numbering of every `<Child/>` mount site in a template — the
|
|
5
|
+
ordinal a child's render-path roots under (`mountChild` on the client, `$$withPath(ordinal)`
|
|
6
|
+
/ `$$renderPath(ordinal)` on the server). ONE walk both back-ends read (the `skeletonContext`
|
|
7
|
+
pattern), replacing the per-back-end `childOrdinal++` counters that numbered components in
|
|
8
|
+
each generator's own EMISSION order — an order that could silently diverge (build emits a
|
|
9
|
+
component's slot content before taking its own ordinal; SSR emits a switch's default case
|
|
10
|
+
after later cases). Numbering from the tree, not the emission, makes SSR↔client ordinal
|
|
11
|
+
congruence hold by construction.
|
|
12
|
+
|
|
13
|
+
Returns the lookup, not the map, so the missing-entry policy lives here too: a miss means a
|
|
14
|
+
back-end reached a mount site the shared walk didn't number — a structural divergence,
|
|
15
|
+
surfaced loudly at compile time rather than as a runtime warm-seed/boundary desync.
|
|
16
|
+
*/
|
|
17
|
+
export function componentOrdinals(nodes: TemplateNode[]): (node: TemplateNode) => number {
|
|
18
|
+
const ordinals = new WeakMap<TemplateNode, number>()
|
|
19
|
+
let next = 0
|
|
20
|
+
const walk = (children: TemplateNode[]): void => {
|
|
21
|
+
for (const child of children) {
|
|
22
|
+
if (child.kind === 'component') {
|
|
23
|
+
ordinals.set(child, next)
|
|
24
|
+
next += 1
|
|
25
|
+
}
|
|
26
|
+
if ('children' in child) {
|
|
27
|
+
walk(child.children)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
walk(nodes)
|
|
32
|
+
return (node: TemplateNode): number => {
|
|
33
|
+
const ordinal = ordinals.get(node)
|
|
34
|
+
if (ordinal === undefined) {
|
|
35
|
+
throw new Error('[abide] component mount site not numbered by the shared ordinal walk')
|
|
36
|
+
}
|
|
37
|
+
return ordinal
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -5,6 +5,7 @@ import { type ReactiveImportBindings, reactiveImportBindings } from './resolveRe
|
|
|
5
5
|
import { signalCallee } from './signalCallee.ts'
|
|
6
6
|
import type { InterpolationKind } from './types/InterpolationKind.ts'
|
|
7
7
|
import type { SeedTypeClassifier } from './types/SeedTypeClassifier.ts'
|
|
8
|
+
import { wrapSeed } from './wrapSeed.ts'
|
|
8
9
|
|
|
9
10
|
/* The routing decision for a no-marker `computed(seed)`: `true` routes to the eager
|
|
10
11
|
`trackedComputed` stream cell (read via `$$readCell`, the `cellReadNames` bucket), `false`
|
|
@@ -14,28 +15,6 @@ type EagerStreamPredicate = (declaration: ts.VariableDeclaration) => boolean
|
|
|
14
15
|
|
|
15
16
|
const factory = ts.factory
|
|
16
17
|
|
|
17
|
-
/* Normalises a `computed`/`linked` argument into a seed THUNK: a literal `() => …` /
|
|
18
|
-
`function` argument passes through unchanged (the author already wrote the thunk), any
|
|
19
|
-
other expression is wrapped as `() => arg`. The wrapper arrow is made ASYNC when the
|
|
20
|
-
expression contains a top-level `await` (`computed(await load())` → `async () => await
|
|
21
|
-
load()`), which is exactly the marker the runtime primitive uses to route to an async cell. */
|
|
22
|
-
function wrapSeed(argument: ts.Expression): ts.Expression {
|
|
23
|
-
if (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) {
|
|
24
|
-
return argument
|
|
25
|
-
}
|
|
26
|
-
const modifiers = hasTopLevelAwait(argument)
|
|
27
|
-
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
|
|
28
|
-
: undefined
|
|
29
|
-
return factory.createArrowFunction(
|
|
30
|
-
modifiers,
|
|
31
|
-
undefined,
|
|
32
|
-
[],
|
|
33
|
-
undefined,
|
|
34
|
-
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
35
|
-
argument,
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
18
|
/* Wraps a seed expression as an async unwrapping thunk `async () => await (arg)` — for a
|
|
40
19
|
type-directed PROMISE seed (ADR-0023/0043) the author wrote WITHOUT `await`
|
|
41
20
|
(`state.computed(getFoo())`). The `async`+`await` makes createAsyncCell unwrap the resolved
|
|
@@ -231,10 +210,6 @@ export function desugarSignals(
|
|
|
231
210
|
derivedNames: Set<string>
|
|
232
211
|
computedNames: Set<string>
|
|
233
212
|
cellReadNames: Set<string>
|
|
234
|
-
/* The full BLOCKING cell set (ADR-0042): the template-injected `await` cells passed in, unioned
|
|
235
|
-
with the script-level `await` computeds/linked collected here. The client template lowering
|
|
236
|
-
reads these via `$$readCellBlocking` (suspend-on-pending). */
|
|
237
|
-
blockingCellNames: Set<string>
|
|
238
213
|
} {
|
|
239
214
|
assertNoRemovedReaders(source)
|
|
240
215
|
/* The full set of names written anywhere (this script + the template). A prop in this set
|
|
@@ -253,10 +228,6 @@ export function desugarSignals(
|
|
|
253
228
|
One read shape covers both — `$$readCell` peeks an async cell and reads `.value` off a
|
|
254
229
|
sync one — so `linked(getStream())` auto-tracks with no read-site branching. */
|
|
255
230
|
const cellReadNames = new Set<string>()
|
|
256
|
-
/* Script-level BLOCKING cell names (ADR-0042 D6): a `computed`/`linked` whose seed carries a
|
|
257
|
-
top-level `await`. Unioned with the template-injected `blockingCellNames` and returned so the
|
|
258
|
-
CLIENT template lowering reads them via `$$readCellBlocking` (suspend-on-pending). */
|
|
259
|
-
const scriptBlockingNames = new Set<string>()
|
|
260
231
|
/* A `props()` destructure must be lowered even when it declares no reactive binding
|
|
261
232
|
(a rest-only `const { ...rest } = props()`), so track its presence on its own. */
|
|
262
233
|
let hasPropsDestructure = false
|
|
@@ -359,27 +330,21 @@ export function desugarSignals(
|
|
|
359
330
|
isEagerStreamComputed(declaration) ||
|
|
360
331
|
isPromiseComputed(declaration)
|
|
361
332
|
) {
|
|
333
|
+
/* Async `computed` (author `await`), an eager stream, or a promise seed — all
|
|
334
|
+
read via `$$readCell(name)`. Whether a pending read PAUSES (author `await`,
|
|
335
|
+
a blocking cell) or peeks `undefined` (streaming) is carried by the cell
|
|
336
|
+
itself at construction (the `trackedComputed(thunk, streaming)` flag), so the
|
|
337
|
+
read form is uniform and nothing blocking-specific is collected here. */
|
|
362
338
|
cellReadNames.add(declaration.name.text)
|
|
363
|
-
/* A blocking async computed (author `await`) reads suspend-on-pending; a bare
|
|
364
|
-
promise (isPromiseComputed) carries no `await` → streaming, so isBlockingSeed
|
|
365
|
-
is false and it never joins this set. */
|
|
366
|
-
const argument = seedArgument(declaration)
|
|
367
|
-
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
368
|
-
scriptBlockingNames.add(declaration.name.text)
|
|
369
|
-
}
|
|
370
339
|
} else {
|
|
371
340
|
computedNames.add(declaration.name.text)
|
|
372
341
|
}
|
|
373
342
|
} else if (callee === 'linked') {
|
|
374
343
|
/* `linked` → a cell read via `$$readCell(name)`: a plain `State` when the seed
|
|
375
344
|
is synchronous, an `AsyncState` when it tracks a promise/stream — one read
|
|
376
|
-
shape auto-tracks whichever source the runtime primitive resolved to.
|
|
345
|
+
shape auto-tracks whichever source the runtime primitive resolved to. Its
|
|
346
|
+
blocking-ness (author `await`) likewise rides on the constructed cell. */
|
|
377
347
|
cellReadNames.add(declaration.name.text)
|
|
378
|
-
/* A blocking async linked (author `await`) reads suspend-on-pending, like computed. */
|
|
379
|
-
const argument = seedArgument(declaration)
|
|
380
|
-
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
381
|
-
scriptBlockingNames.add(declaration.name.text)
|
|
382
|
-
}
|
|
383
348
|
} else if (callee === 'state') {
|
|
384
349
|
/* `state(initial, transform)` → a `.value` cell (its write-coercion transform
|
|
385
350
|
forces a local store); referenced as `name.value`, unchanged. */
|
|
@@ -441,7 +406,6 @@ export function desugarSignals(
|
|
|
441
406
|
derivedNames,
|
|
442
407
|
computedNames,
|
|
443
408
|
cellReadNames,
|
|
444
|
-
blockingCellNames: new Set([...blockingCellNames, ...scriptBlockingNames]),
|
|
445
409
|
}
|
|
446
410
|
}
|
|
447
411
|
|
|
@@ -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,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
|
|