@abide/abide 0.51.0 → 0.53.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 +518 -525
- package/CHANGELOG.md +54 -0
- package/README.md +130 -157
- package/package.json +5 -1
- package/src/abideResolverPlugin.ts +12 -0
- package/src/checkAbide.ts +60 -7
- package/src/lib/cli/completeCli.ts +38 -0
- package/src/lib/cli/printTopLevelHelp.ts +1 -0
- package/src/lib/cli/renderCliCompletions.ts +56 -0
- package/src/lib/cli/runCli.ts +27 -0
- package/src/lib/server/render.ts +70 -0
- package/src/lib/server/runtime/cacheStalenessBroadcaster.ts +33 -0
- package/src/lib/server/runtime/createServer.ts +43 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +37 -0
- package/src/lib/server/runtime/pageRenderSlot.ts +15 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/registerSocket.ts +12 -0
- package/src/lib/shared/CACHE_STALENESS_SOCKET.ts +8 -0
- package/src/lib/shared/RESERVED_SOCKET_PREFIX.ts +8 -0
- package/src/lib/shared/applyCacheStalenessLocally.ts +18 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +3 -1
- package/src/lib/shared/cache.ts +32 -4
- package/src/lib/shared/cacheStalenessSlot.ts +21 -0
- package/src/lib/shared/createRpcServerProgram.ts +187 -0
- package/src/lib/shared/docSnapshotsSlot.ts +13 -0
- package/src/lib/shared/invalidate.ts +39 -0
- package/src/lib/shared/matcherFromEnvelope.ts +31 -0
- package/src/lib/shared/prepareRpcModule.ts +22 -3
- package/src/lib/shared/refresh.ts +9 -2
- package/src/lib/shared/selectorMatcher.ts +2 -15
- package/src/lib/shared/serializeSelector.ts +69 -0
- package/src/lib/shared/setsIntersect.ts +15 -0
- package/src/lib/shared/types/CacheStalenessApply.ts +13 -0
- package/src/lib/shared/types/CacheStalenessFrame.ts +24 -0
- package/src/lib/shared/types/DocSnapshots.ts +12 -0
- package/src/lib/shared/types/RemoteFunction.ts +11 -8
- package/src/lib/shared/types/RpcBuildStamps.ts +9 -0
- package/src/lib/shared/types/SsrPayload.ts +5 -0
- package/src/lib/test/createTestApp.ts +15 -0
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +19 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +2 -0
- package/src/lib/ui/compile/analyzeComponent.ts +2 -0
- package/src/lib/ui/compile/compileComponent.ts +3 -1
- package/src/lib/ui/compile/compileSSR.ts +12 -0
- package/src/lib/ui/compile/compileShadow.ts +97 -16
- package/src/lib/ui/compile/desugarSignals.ts +60 -32
- package/src/lib/ui/compile/generateBuild.ts +5 -1
- package/src/lib/ui/compile/hasTopLevelAwait.ts +28 -0
- package/src/lib/ui/compile/isFunctionScopeBoundary.ts +20 -0
- package/src/lib/ui/compile/liftAsyncSubExpressions.ts +2 -2
- package/src/lib/ui/compile/lowerContext.ts +4 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -14
- package/src/lib/ui/compile/lowerScript.ts +17 -2
- package/src/lib/ui/compile/renameSignalRefs.ts +83 -0
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
- package/src/lib/ui/compile/wrapReactionCellSources.ts +32 -0
- package/src/lib/ui/createScope.ts +42 -1
- package/src/lib/ui/dom/appendText.ts +29 -8
- package/src/lib/ui/dom/appendTextAt.ts +21 -4
- package/src/lib/ui/dom/assertClaimedText.ts +14 -4
- package/src/lib/ui/dom/attr.ts +41 -3
- package/src/lib/ui/dom/awaitBlock.ts +34 -13
- package/src/lib/ui/dom/each.ts +10 -2
- package/src/lib/ui/dom/eachAsync.ts +9 -1
- package/src/lib/ui/dom/readCellBlocking.ts +29 -0
- package/src/lib/ui/dom/readTextOrSuspend.ts +21 -0
- package/src/lib/ui/dom/spreadAttrs.ts +47 -1
- package/src/lib/ui/dom/switchBlock.ts +19 -2
- package/src/lib/ui/dom/tryBlock.ts +10 -0
- package/src/lib/ui/dom/when.ts +19 -1
- package/src/lib/ui/dom/withSuspense.ts +22 -0
- package/src/lib/ui/dom/writeCell.ts +22 -0
- package/src/lib/ui/runtime/DOC_SEED.ts +11 -0
- package/src/lib/ui/runtime/SuspenseSignal.ts +24 -0
- package/src/lib/ui/runtime/claimExpected.ts +6 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -10
- package/src/lib/ui/runtime/reportHydrationDivergence.ts +30 -0
- package/src/lib/ui/startClient.ts +22 -1
- package/src/lib/ui/subscribeCacheStaleness.ts +85 -0
- package/src/lib/ui/watch.ts +7 -2
- package/src/serverEntry.ts +12 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Maps a compound-assignment operator to its plain binary counterpart, for lowering
|
|
5
|
+
`x += y` into an unconditional read-combine-write. Logical assignments
|
|
6
|
+
(`||=`/`&&=`/`??=`) lower the same way — the patch/cell write always fires,
|
|
7
|
+
consistent with how `+=` lowers. Shared by lowerDocAccess (the `$$model` write path)
|
|
8
|
+
and renameSignalRefs (the `$$writeCell` linked-cell path) so the two lowerings can't
|
|
9
|
+
drift.
|
|
10
|
+
*/
|
|
11
|
+
export const COMPOUND_ASSIGNMENT_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
12
|
+
[ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken],
|
|
13
|
+
[ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken],
|
|
14
|
+
[ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken],
|
|
15
|
+
[ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken],
|
|
16
|
+
[ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken],
|
|
17
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken],
|
|
18
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
19
|
+
])
|
|
@@ -56,6 +56,8 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: stri
|
|
|
56
56
|
{ name: 'spreadAttrs', specifier: 'ui/dom/spreadAttrs', alias: '$$spreadAttrs' },
|
|
57
57
|
{ name: 'readCall', specifier: 'ui/dom/readCall', alias: '$$readCall' },
|
|
58
58
|
{ name: 'readCell', specifier: 'ui/dom/readCell', alias: '$$readCell' },
|
|
59
|
+
{ name: 'readCellBlocking', specifier: 'ui/dom/readCellBlocking', alias: '$$readCellBlocking' },
|
|
60
|
+
{ name: 'writeCell', specifier: 'ui/dom/writeCell', alias: '$$writeCell' },
|
|
59
61
|
{ name: 'cellPending', specifier: 'ui/dom/cellPending', alias: '$$cellPending' },
|
|
60
62
|
{ name: 'withPath', specifier: 'ui/runtime/withPath', alias: '$$withPath' },
|
|
61
63
|
{ name: 'settleAsyncCells', specifier: 'ui/settleAsyncCells', alias: '$$settleAsyncCells' },
|
|
@@ -120,6 +120,7 @@ export function analyzeComponent(
|
|
|
120
120
|
derivedNames,
|
|
121
121
|
computedNames,
|
|
122
122
|
cellReadNames,
|
|
123
|
+
blockingCellNames: allBlockingCellNames,
|
|
123
124
|
droppedReactiveImports,
|
|
124
125
|
} = lowerScript(
|
|
125
126
|
fullScriptBody,
|
|
@@ -142,6 +143,7 @@ export function analyzeComponent(
|
|
|
142
143
|
derivedNames,
|
|
143
144
|
computedNames,
|
|
144
145
|
cellReadNames,
|
|
146
|
+
blockingCellNames: allBlockingCellNames,
|
|
145
147
|
nodes,
|
|
146
148
|
styles,
|
|
147
149
|
/* Hydration adopts every block in place — including `await`, which resumes
|
|
@@ -29,7 +29,8 @@ 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 { script, stateNames, derivedNames, computedNames, cellReadNames, nodes } =
|
|
32
|
+
const { script, stateNames, derivedNames, computedNames, cellReadNames, blockingCellNames, nodes } =
|
|
33
|
+
resolved
|
|
33
34
|
const build = generateBuild(
|
|
34
35
|
nodes,
|
|
35
36
|
'host',
|
|
@@ -38,6 +39,7 @@ export function compileComponent(
|
|
|
38
39
|
computedNames,
|
|
39
40
|
isLayout,
|
|
40
41
|
cellReadNames,
|
|
42
|
+
blockingCellNames,
|
|
41
43
|
)
|
|
42
44
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
43
45
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
@@ -66,6 +66,18 @@ export function compileSSR(
|
|
|
66
66
|
flightDecls,
|
|
67
67
|
hasStagedChildren,
|
|
68
68
|
} = generateSSR(nodes, stateNames, derivedNames, computedNames, isLayout, cellReadNames)
|
|
69
|
+
/* ADR-0042 D7 (barrier completeness): every async cell — and so every BLOCKING (`await`) cell —
|
|
70
|
+
is declared in the lowered script (`lowered`), which the body below emits BEFORE the
|
|
71
|
+
`$$settleAsyncCells` barrier; the template back-end (`ssr`) constructs no cells. A blocking
|
|
72
|
+
cell built AFTER its barrier would ship an unresolved value the client then SUSPENDS against,
|
|
73
|
+
desyncing hydration. The invariant holds by construction today (cells are a script-block
|
|
74
|
+
construct); assert it as a tripwire so a future template lowering that emits a cell
|
|
75
|
+
construction fails the build LOUDLY here instead of as a runtime hydration desync. */
|
|
76
|
+
if (/\$\$scope\(\)\.(trackedComputed|computed|linked)\(/.test(ssr)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'[abide] a reactive cell is constructed in template output, after the async-cell barrier (ADR-0042 D7) — cells must be declared in the component script so they settle before the SSR flush.',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
69
81
|
/* ADR-0039: a component with hoistable children declares `$childSlots` (the body walk reserves an
|
|
70
82
|
output slot per child) and, after the walk, awaits `$$finalizeStreamedChildren` — which fills
|
|
71
83
|
each slot inline (settled child, byte-identical to the old inline await) or with a streaming
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { ABIDE_PACKAGE_NAME } from '../../shared/ABIDE_PACKAGE_NAME.ts'
|
|
3
3
|
import { attrLiftPosition } from './attrLiftPosition.ts'
|
|
4
|
+
import { hasTopLevelAwait } from './hasTopLevelAwait.ts'
|
|
5
|
+
import { isFunctionScopeBoundary } from './isFunctionScopeBoundary.ts'
|
|
4
6
|
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
5
7
|
import { type LiftPosition, liftAsyncSubExpressions } from './liftAsyncSubExpressions.ts'
|
|
6
8
|
import { parseTemplate } from './parseTemplate.ts'
|
|
@@ -94,6 +96,23 @@ export function compileShadow(
|
|
|
94
96
|
watchLocalName,
|
|
95
97
|
} = analyzeScript(scriptBody, scriptStart)
|
|
96
98
|
builder.raw(shadowPreamble(importedReactives))
|
|
99
|
+
/* Every `computed`/`linked` scope line reads through this: it invokes the seed thunk and
|
|
100
|
+
unwraps a promise/stream to the value a BARE cell read peeks (ADR-0019/0032) — so an async
|
|
101
|
+
`computed(await …)` / stream `computed(src)` type-checks as its resolved value, and a sync
|
|
102
|
+
seed keeps its return type (`Awaited` is identity on a non-thenable). Reserved `$$` name so
|
|
103
|
+
it can never collide with an author binding; `declare`-only, so it carries no runtime. */
|
|
104
|
+
builder.raw(
|
|
105
|
+
'declare function $$cellValue<R>(seed: () => R): R extends AsyncIterable<infer F> ? F : Awaited<R>\n',
|
|
106
|
+
)
|
|
107
|
+
/* The STREAMING sibling (ADR-0042): a no-`await` async `computed`/`linked` cell reads
|
|
108
|
+
`undefined`-while-pending, so its shadow type is the resolved value UNIONED with `undefined` —
|
|
109
|
+
the author must guard it (`?.`/`??`), exactly the runtime. A BLOCKING (`await`) cell uses
|
|
110
|
+
`$$cellValue` above (resolved `T`, never pending — its render region suspends). Selected in
|
|
111
|
+
`scopeLineFor` by the syntactic blocking bit. Unconditional (a named cell can occur with no
|
|
112
|
+
classifier), matching `$$cellValue`. */
|
|
113
|
+
builder.raw(
|
|
114
|
+
'declare function $$cellValuePending<R>(seed: () => R): (R extends AsyncIterable<infer F> ? F : Awaited<R>) | undefined\n',
|
|
115
|
+
)
|
|
97
116
|
/* The peek helpers the async-interpolation wrap targets (ADR-0032): `$$peek` unwraps a promise
|
|
98
117
|
sub-expression to its resolved value (`undefined` while pending) and `$$peekStream` reads an
|
|
99
118
|
async iterable's latest frame, so `getFoo()?.name`/`{#if getFoo()}` type-check against the
|
|
@@ -395,27 +414,38 @@ function collectReservedNameDiagnostics(
|
|
|
395
414
|
top-level await transpiles to `await` in a non-async function and breaks the bundle; the
|
|
396
415
|
shadow's render fn is async, so `tsc` alone never flags it (a check/runtime parity gap).
|
|
397
416
|
Stops descending at function boundaries — their own async-ness is the author's concern —
|
|
398
|
-
and catches both `await expr` and `for await (… of …)`, flagging the `await` keyword.
|
|
417
|
+
and catches both `await expr` and `for await (… of …)`, flagging the `await` keyword. A
|
|
418
|
+
`computed`/`linked` SEED argument is exempt: `wrapSeed` (desugarSignals) lowers a top-level
|
|
419
|
+
`await` there into an async thunk (`computed(await load())` → `async () => await load()`), so
|
|
420
|
+
its await runs off the sync build — flagging it would be a false positive. */
|
|
399
421
|
function collectTopLevelAwaitDiagnostics(
|
|
400
422
|
file: ts.SourceFile,
|
|
401
423
|
scriptStart: number,
|
|
402
424
|
diagnostics: ShadowDiagnostic[],
|
|
425
|
+
bindings: ReactiveImportBindings,
|
|
403
426
|
): void {
|
|
404
427
|
const message =
|
|
405
428
|
'top-level `await` is not allowed in a `<script>` — the component build runs synchronously. Use `{#await expr}…{:then value}…{/await}` markup for blocking data, or `await` inside an async event handler.'
|
|
406
429
|
const visit = (node: ts.Node): void => {
|
|
407
430
|
/* A nested function introduces its own (possibly async) scope; its awaits are legal. */
|
|
408
|
-
if (
|
|
409
|
-
ts.isFunctionDeclaration(node) ||
|
|
410
|
-
ts.isFunctionExpression(node) ||
|
|
411
|
-
ts.isArrowFunction(node) ||
|
|
412
|
-
ts.isMethodDeclaration(node) ||
|
|
413
|
-
ts.isGetAccessorDeclaration(node) ||
|
|
414
|
-
ts.isSetAccessorDeclaration(node) ||
|
|
415
|
-
ts.isConstructorDeclaration(node)
|
|
416
|
-
) {
|
|
431
|
+
if (isFunctionScopeBoundary(node)) {
|
|
417
432
|
return
|
|
418
433
|
}
|
|
434
|
+
/* A `computed`/`linked` seed becomes an async thunk (`wrapSeed`), so an await in it is
|
|
435
|
+
legal — visit the callee and any trailing args, but skip the seed (`arguments[0]`). */
|
|
436
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
437
|
+
const callee = resolveReactiveExport(node.expression, bindings)
|
|
438
|
+
if (callee === 'computed' || callee === 'linked') {
|
|
439
|
+
visit(node.expression)
|
|
440
|
+
for (let index = 1; index < node.arguments.length; index++) {
|
|
441
|
+
const argument = node.arguments[index]
|
|
442
|
+
if (argument !== undefined) {
|
|
443
|
+
visit(argument)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
}
|
|
419
449
|
/* The `await` keyword token: the AwaitExpression's first child, or a `for await`'s
|
|
420
450
|
await modifier. */
|
|
421
451
|
const keyword = ts.isAwaitExpression(node)
|
|
@@ -449,7 +479,8 @@ function collectNestedScriptAwaitDiagnostics(
|
|
|
449
479
|
}
|
|
450
480
|
if (node.kind === 'script' && node.loc !== undefined) {
|
|
451
481
|
const file = ts.createSourceFile('nested.ts', node.code, ts.ScriptTarget.Latest, true)
|
|
452
|
-
|
|
482
|
+
/* A nested script inherits the surface by canonical name (no imports of its own). */
|
|
483
|
+
collectTopLevelAwaitDiagnostics(file, node.loc, diagnostics, NESTED_REACTIVE_BINDINGS)
|
|
453
484
|
}
|
|
454
485
|
if ('children' in node) {
|
|
455
486
|
collectNestedScriptAwaitDiagnostics(node.children, diagnostics)
|
|
@@ -503,7 +534,7 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
503
534
|
collectReservedNameDiagnostics(file, scriptStart, diagnostics)
|
|
504
535
|
/* The leading script runs in the synchronous `build()`, so a top-level await breaks the
|
|
505
536
|
bundle — catch it here with a legible message instead of an opaque transpile error. */
|
|
506
|
-
collectTopLevelAwaitDiagnostics(file, scriptStart, diagnostics)
|
|
537
|
+
collectTopLevelAwaitDiagnostics(file, scriptStart, diagnostics, bindings)
|
|
507
538
|
/* A verbatim span: original text + the segment mapping it back, relative to the
|
|
508
539
|
line start (the caller rebases shadowStart onto the running shadow length). */
|
|
509
540
|
const span = (node: ts.Node, prefixLength: number): ScopeLine['segments'][number] => ({
|
|
@@ -683,13 +714,57 @@ function scopeLineFor(
|
|
|
683
714
|
segments: [span(declaration.name, keywordOffset)],
|
|
684
715
|
})
|
|
685
716
|
}
|
|
686
|
-
|
|
717
|
+
/* Mirror `wrapSeed` (desugarSignals): a bare (non-thunk) seed is normalised to `() => (seed)`,
|
|
718
|
+
made ASYNC when it carries a top-level `await` — so `computed(await load())` projects as a
|
|
719
|
+
legal async thunk instead of a raw top-level await (`(await load())()` — a build-breaker that
|
|
720
|
+
also mis-called the resolved value). A literal thunk passes through unchanged. `$$cellValue`
|
|
721
|
+
then invokes it and unwraps a promise/stream to the value a bare read peeks, so an async or
|
|
722
|
+
stream seed types as its RESOLVED value (`Promise<T>` → `T`) rather than the raw promise. */
|
|
723
|
+
const fnIsThunk = ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)
|
|
724
|
+
const wrapPrefix = fnIsThunk ? '' : seedIsAsync(fn) ? 'async () => (' : '() => ('
|
|
725
|
+
const wrapSuffix = fnIsThunk ? '' : ')'
|
|
726
|
+
/* ADR-0042: a STREAMING promise cell (async seed, no top-level `await`) reads
|
|
727
|
+
`undefined`-while-pending → `$$cellValuePending` (resolved `T | undefined`). A BLOCKING
|
|
728
|
+
(`await`) cell suspends its region so it is always resolved, and a sync cell is never pending
|
|
729
|
+
→ `$$cellValue` (resolved `T`). (A bare stream seed keeps `$$cellValue`'s frame type — the
|
|
730
|
+
pre-existing type-directed case, unchanged here.) */
|
|
731
|
+
const helper = seedIsAsync(fn) && !seedIsBlocking(fn) ? '$$cellValuePending' : '$$cellValue'
|
|
732
|
+
const prefix = `${keyword} ${name}${annotation} = ${helper}(${wrapPrefix}`
|
|
687
733
|
return withCalleeRef({
|
|
688
|
-
text: `${prefix}${verbatim(fn)})
|
|
734
|
+
text: `${prefix}${verbatim(fn)}${wrapSuffix});`,
|
|
689
735
|
segments: [span(declaration.name, keywordOffset), span(fn, prefix.length)],
|
|
690
736
|
})
|
|
691
737
|
}
|
|
692
738
|
|
|
739
|
+
/* True for a `computed`/`linked` seed that `wrapSeed` (desugarSignals) turns into an ASYNC thunk —
|
|
740
|
+
a thunk the author wrote `async`, or a bare seed carrying a top-level `await` (`computed(await
|
|
741
|
+
x)`). The shadow must agree so it unwraps the promise to the resolved value AND so the top-level
|
|
742
|
+
await scan spares the seed. Stops at function boundaries — an inner `async` callback is its own
|
|
743
|
+
scope, mirroring `hasTopLevelAwait`. */
|
|
744
|
+
function seedIsAsync(seed: ts.Expression): boolean {
|
|
745
|
+
if (ts.isArrowFunction(seed) || ts.isFunctionExpression(seed)) {
|
|
746
|
+
return (
|
|
747
|
+
ts
|
|
748
|
+
.getModifiers(seed)
|
|
749
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ?? false
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
return hasTopLevelAwait(seed)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* True for a BLOCKING async cell (ADR-0042 D6): an async thunk whose BODY has a top-level `await`
|
|
756
|
+
(`computed(async () => await X)`), or a bare seed carrying a top-level `await` (`computed(await
|
|
757
|
+
X)` — wrapSeed makes it an async thunk over that await). An async thunk with NO await
|
|
758
|
+
(`computed(async () => getFoo())`) is STREAMING, not blocking. Shares `hasTopLevelAwait` with
|
|
759
|
+
desugarSignals' `isBlockingSeed` so the shadow type (`$$cellValue` vs `$$cellValuePending`) agrees
|
|
760
|
+
with the runtime's blocking flag — the "one predicate" ADR-0042 D5 requires. */
|
|
761
|
+
function seedIsBlocking(seed: ts.Expression): boolean {
|
|
762
|
+
if (ts.isArrowFunction(seed) || ts.isFunctionExpression(seed)) {
|
|
763
|
+
return seedIsAsync(seed) && hasTopLevelAwait(seed.body)
|
|
764
|
+
}
|
|
765
|
+
return hasTopLevelAwait(seed)
|
|
766
|
+
}
|
|
767
|
+
|
|
693
768
|
/* Whether any ELEMENT in the tree carries an `attach` — gates emitting the DOM-typed
|
|
694
769
|
attachment aliases. All nested content (block bodies, await/switch branches, snippet
|
|
695
770
|
bodies) routes through `children`, so a recursive `children` walk is complete. */
|
|
@@ -969,7 +1044,11 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
969
1044
|
} else if (branch.branch === 'catch' && branch.as !== undefined) {
|
|
970
1045
|
builder.raw('const ')
|
|
971
1046
|
builder.mapped(branch.as, branch.asLoc)
|
|
972
|
-
|
|
1047
|
+
/* `unknown`, not `any` — a `catch` error is statically unknowable, and this
|
|
1048
|
+
matches what strict-mode plain TS gives a real `catch (err)`
|
|
1049
|
+
(`useUnknownInCatchVariables`), so the author must narrow before dereferencing
|
|
1050
|
+
(`err instanceof Error ? … : …`) exactly as outside a template (ADR-0042 fidelity). */
|
|
1051
|
+
builder.raw(' = undefined as unknown;\n')
|
|
973
1052
|
}
|
|
974
1053
|
emitNodes(branch.children, builder)
|
|
975
1054
|
builder.raw('}\n')
|
|
@@ -1015,7 +1094,9 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
1015
1094
|
handlers consume their own branches inline and never route through this case. */
|
|
1016
1095
|
builder.raw('{\n')
|
|
1017
1096
|
if (node.as !== undefined) {
|
|
1018
|
-
|
|
1097
|
+
/* `unknown`, not `any` — see the await-`catch` binding above (matches strict TS's
|
|
1098
|
+
`catch (err)`, forcing the author to narrow). */
|
|
1099
|
+
builder.raw(`const ${node.as} = undefined as unknown;\n`)
|
|
1019
1100
|
}
|
|
1020
1101
|
emitNodes(node.children, builder)
|
|
1021
1102
|
builder.raw('}\n')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { assignmentTargetNames } from './assignmentTargetNames.ts'
|
|
3
|
+
import { hasTopLevelAwait } from './hasTopLevelAwait.ts'
|
|
3
4
|
import { type ReactiveImportBindings, reactiveImportBindings } from './resolveReactiveExport.ts'
|
|
4
5
|
import { signalCallee } from './signalCallee.ts'
|
|
5
6
|
import type { InterpolationKind } from './types/InterpolationKind.ts'
|
|
@@ -13,34 +14,6 @@ type EagerStreamPredicate = (declaration: ts.VariableDeclaration) => boolean
|
|
|
13
14
|
|
|
14
15
|
const factory = ts.factory
|
|
15
16
|
|
|
16
|
-
/* True when `node` holds an `await` that runs at the seed's own top level — one NOT nested
|
|
17
|
-
inside a further function. The walk stops at every nested function boundary, so an
|
|
18
|
-
`await` in an inner callback (`items.map(async (x) => await f(x))`) does not count: only
|
|
19
|
-
a top-level `await` marks the seed itself as an async thunk the async-cell path unwraps. */
|
|
20
|
-
function hasTopLevelAwait(node: ts.Node): boolean {
|
|
21
|
-
let found = false
|
|
22
|
-
const visit = (child: ts.Node): void => {
|
|
23
|
-
if (found) {
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
/* A nested function is its own await scope — don't descend into it. */
|
|
27
|
-
if (
|
|
28
|
-
ts.isFunctionDeclaration(child) ||
|
|
29
|
-
ts.isFunctionExpression(child) ||
|
|
30
|
-
ts.isArrowFunction(child)
|
|
31
|
-
) {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
if (ts.isAwaitExpression(child)) {
|
|
35
|
-
found = true
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
ts.forEachChild(child, visit)
|
|
39
|
-
}
|
|
40
|
-
visit(node)
|
|
41
|
-
return found
|
|
42
|
-
}
|
|
43
|
-
|
|
44
17
|
/* Normalises a `computed`/`linked` argument into a seed THUNK: a literal `() => …` /
|
|
45
18
|
`function` argument passes through unchanged (the author already wrote the thunk), any
|
|
46
19
|
other expression is wrapped as `() => arg`. The wrapper arrow is made ASYNC when the
|
|
@@ -106,6 +79,23 @@ function isBareCallComputed(declaration: ts.VariableDeclaration): boolean {
|
|
|
106
79
|
return ts.isCallExpression(argument) || ts.isIdentifier(argument)
|
|
107
80
|
}
|
|
108
81
|
|
|
82
|
+
/* True when a `computed`/`linked` seed is a BLOCKING async cell (ADR-0042 D6): its wrapped seed
|
|
83
|
+
is an async thunk whose BODY has a top-level `await` — `computed(await X)` (wrapSeed makes it
|
|
84
|
+
`async () => await X`) or `computed(async () => await X)`. An async thunk with NO await
|
|
85
|
+
(`computed(async () => getFoo())`) is STREAMING — `await` is the sole blocking marker. A
|
|
86
|
+
sync/stream seed is not blocking. The read of a blocking cell suspends its render region until
|
|
87
|
+
the value resolves; a streaming cell reads `undefined`-while-pending. Reuses `hasTopLevelAwait`
|
|
88
|
+
applied to the thunk BODY (not the arrow), so the walk descends exactly one function level. */
|
|
89
|
+
function isBlockingSeed(argument: ts.Expression): boolean {
|
|
90
|
+
const wrapped = wrapSeed(argument)
|
|
91
|
+
if (!isAsyncSeed(wrapped)) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
const body =
|
|
95
|
+
ts.isArrowFunction(wrapped) || ts.isFunctionExpression(wrapped) ? wrapped.body : wrapped
|
|
96
|
+
return hasTopLevelAwait(body)
|
|
97
|
+
}
|
|
98
|
+
|
|
109
99
|
/* Emits a compile WARNING (best-effort, never fatal) when a signal read appears AFTER the
|
|
110
100
|
first top-level `await` in an async seed — a value read there is no longer tracked, so the
|
|
111
101
|
cell won't reseed when it changes. Detection only: reads before the await (or with no
|
|
@@ -223,6 +213,10 @@ export function desugarSignals(
|
|
|
223
213
|
derivedNames: Set<string>
|
|
224
214
|
computedNames: Set<string>
|
|
225
215
|
cellReadNames: Set<string>
|
|
216
|
+
/* The full BLOCKING cell set (ADR-0042): the template-injected `await` cells passed in, unioned
|
|
217
|
+
with the script-level `await` computeds/linked collected here. The client template lowering
|
|
218
|
+
reads these via `$$readCellBlocking` (suspend-on-pending). */
|
|
219
|
+
blockingCellNames: Set<string>
|
|
226
220
|
} {
|
|
227
221
|
assertNoRemovedReaders(source)
|
|
228
222
|
/* The full set of names written anywhere (this script + the template). A prop in this set
|
|
@@ -241,6 +235,10 @@ export function desugarSignals(
|
|
|
241
235
|
One read shape covers both — `$$readCell` peeks an async cell and reads `.value` off a
|
|
242
236
|
sync one — so `linked(getStream())` auto-tracks with no read-site branching. */
|
|
243
237
|
const cellReadNames = new Set<string>()
|
|
238
|
+
/* Script-level BLOCKING cell names (ADR-0042 D6): a `computed`/`linked` whose seed carries a
|
|
239
|
+
top-level `await`. Unioned with the template-injected `blockingCellNames` and returned so the
|
|
240
|
+
CLIENT template lowering reads them via `$$readCellBlocking` (suspend-on-pending). */
|
|
241
|
+
const scriptBlockingNames = new Set<string>()
|
|
244
242
|
/* A `props()` destructure must be lowered even when it declares no reactive binding
|
|
245
243
|
(a rest-only `const { ...rest } = props()`), so track its presence on its own. */
|
|
246
244
|
let hasPropsDestructure = false
|
|
@@ -325,6 +323,11 @@ export function desugarSignals(
|
|
|
325
323
|
or its seed type resolves to a stream (`isEagerStreamComputed`, ADR-0023). */
|
|
326
324
|
if (isAsyncComputed(declaration) || isEagerStreamComputed(declaration)) {
|
|
327
325
|
cellReadNames.add(declaration.name.text)
|
|
326
|
+
/* A blocking async computed (author `await`) reads suspend-on-pending. */
|
|
327
|
+
const argument = seedArgument(declaration)
|
|
328
|
+
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
329
|
+
scriptBlockingNames.add(declaration.name.text)
|
|
330
|
+
}
|
|
328
331
|
} else {
|
|
329
332
|
computedNames.add(declaration.name.text)
|
|
330
333
|
}
|
|
@@ -333,6 +336,11 @@ export function desugarSignals(
|
|
|
333
336
|
is synchronous, an `AsyncState` when it tracks a promise/stream — one read
|
|
334
337
|
shape auto-tracks whichever source the runtime primitive resolved to. */
|
|
335
338
|
cellReadNames.add(declaration.name.text)
|
|
339
|
+
/* A blocking async linked (author `await`) reads suspend-on-pending, like computed. */
|
|
340
|
+
const argument = seedArgument(declaration)
|
|
341
|
+
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
342
|
+
scriptBlockingNames.add(declaration.name.text)
|
|
343
|
+
}
|
|
336
344
|
} else if (callee === 'state') {
|
|
337
345
|
/* `state(initial, transform)` → a `.value` cell (its write-coercion transform
|
|
338
346
|
forces a local store); referenced as `name.value`, unchanged. */
|
|
@@ -387,7 +395,14 @@ export function desugarSignals(
|
|
|
387
395
|
return factory.updateSourceFile(root, statements)
|
|
388
396
|
}
|
|
389
397
|
|
|
390
|
-
return {
|
|
398
|
+
return {
|
|
399
|
+
transformer,
|
|
400
|
+
stateNames,
|
|
401
|
+
derivedNames,
|
|
402
|
+
computedNames,
|
|
403
|
+
cellReadNames,
|
|
404
|
+
blockingCellNames: new Set([...blockingCellNames, ...scriptBlockingNames]),
|
|
405
|
+
}
|
|
391
406
|
}
|
|
392
407
|
|
|
393
408
|
/* The lowered form of a top-level statement: state slots → `model.x = init`
|
|
@@ -627,10 +642,23 @@ function computedStatements(
|
|
|
627
642
|
)
|
|
628
643
|
: wrapSeed(argument)
|
|
629
644
|
if (isAsyncSeed(wrapped)) {
|
|
630
|
-
/* Async seed → the eager
|
|
631
|
-
|
|
645
|
+
/* Async seed → the eager async cell (`AsyncComputed`, read via `$$readCell`), the
|
|
646
|
+
runtime unwraps the promise. `await` in the thunk body → BLOCKING (joins the SSR
|
|
647
|
+
barrier, resolves inline, client suspends its render region); async modifier with NO
|
|
648
|
+
await → STREAMING (ships pending, resolves on the client) — ADR-0042 D6. Routed
|
|
649
|
+
through `trackedComputed(thunk, streaming)` (same createAsyncCell for an async thunk)
|
|
650
|
+
to carry the flag, matching the template-injected path. */
|
|
632
651
|
warnPostAwaitReads(wrapped, signalNames)
|
|
633
|
-
|
|
652
|
+
const streaming = argument === undefined || !isBlockingSeed(argument)
|
|
653
|
+
statements.push(
|
|
654
|
+
constDeclaration(
|
|
655
|
+
name,
|
|
656
|
+
scopeMethodCall('trackedComputed', [
|
|
657
|
+
wrapped,
|
|
658
|
+
streaming ? factory.createTrue() : factory.createFalse(),
|
|
659
|
+
]),
|
|
660
|
+
),
|
|
661
|
+
)
|
|
634
662
|
} else if (isEagerStreamComputed(declaration)) {
|
|
635
663
|
/* Stream seed → the eager `trackedComputed`, which probes the seed and auto-tracks a
|
|
636
664
|
stream (`AsyncComputed`) or falls back to a lazy computed; read via `$$readCell`.
|
|
@@ -77,6 +77,10 @@ export function generateBuild(
|
|
|
77
77
|
isLayout = false,
|
|
78
78
|
/* `linked` / async `computed` names, lowered to `$$readCell(name)` in template exprs. */
|
|
79
79
|
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(),
|
|
80
84
|
): string {
|
|
81
85
|
const nextVar = makeVarNamer()
|
|
82
86
|
/* A per-body source-order ordinal for each `<Child/>` mount site, so two same-type siblings
|
|
@@ -121,7 +125,7 @@ export function generateBuild(
|
|
|
121
125
|
withShadow,
|
|
122
126
|
bindRead,
|
|
123
127
|
bindWrite,
|
|
124
|
-
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames)
|
|
128
|
+
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames, blockingCellNames)
|
|
125
129
|
|
|
126
130
|
/* Maps a plan `Binding`'s classification to the client `ShadowKind`: a `reactive` value
|
|
127
131
|
derefs as a `.value` cell (`derived`), a `plain` value as the bare local (`plain`). The
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { isFunctionScopeBoundary } from './isFunctionScopeBoundary.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
True when `node` contains an `await` at its OWN top level — not nested inside any function scope
|
|
6
|
+
(`isFunctionScopeBoundary`: function/arrow/method/accessor/constructor). The SINGLE seed classifier
|
|
7
|
+
both the build lowering (`desugarSignals`: `await` present → a BLOCKING cell that suspends its render
|
|
8
|
+
region) and the type shadow (`compileShadow`: `$$cellValue` `T` vs `$$cellValuePending` `T | undefined`)
|
|
9
|
+
share, so the runtime blocking flag and the shadow type can never disagree on whether a seed is
|
|
10
|
+
`await`-marked (ADR-0042 D5's "one predicate"). An `await` in an inner callback / method
|
|
11
|
+
(`items.map(async (x) => await f(x))`, `{ async m() { await x } }`) does NOT count — only a
|
|
12
|
+
top-level `await` marks the seed itself.
|
|
13
|
+
*/
|
|
14
|
+
export function hasTopLevelAwait(node: ts.Node): boolean {
|
|
15
|
+
let found = false
|
|
16
|
+
const visit = (child: ts.Node): void => {
|
|
17
|
+
if (found || isFunctionScopeBoundary(child)) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (ts.isAwaitExpression(child)) {
|
|
21
|
+
found = true
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
ts.forEachChild(child, visit)
|
|
25
|
+
}
|
|
26
|
+
visit(node)
|
|
27
|
+
return found
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A node that opens its own (possibly async) function scope — awaits inside it are that scope's
|
|
5
|
+
concern, not the enclosing seed's. Both the top-level-await walk (`hasTopLevelAwait`) and the
|
|
6
|
+
top-level-await diagnostics stop descending here, so a seed classifies the same way a value read
|
|
7
|
+
inside such a scope would behave. One predicate shared by the build lowering and the type shadow so
|
|
8
|
+
they can never disagree on a boundary (ADR-0042 D5's "one predicate").
|
|
9
|
+
*/
|
|
10
|
+
export function isFunctionScopeBoundary(node: ts.Node): boolean {
|
|
11
|
+
return (
|
|
12
|
+
ts.isFunctionDeclaration(node) ||
|
|
13
|
+
ts.isFunctionExpression(node) ||
|
|
14
|
+
ts.isArrowFunction(node) ||
|
|
15
|
+
ts.isMethodDeclaration(node) ||
|
|
16
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
17
|
+
ts.isSetAccessorDeclaration(node) ||
|
|
18
|
+
ts.isConstructorDeclaration(node)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -101,8 +101,8 @@ export function liftAsyncSubExpressions(
|
|
|
101
101
|
/* A nested function is its own evaluation scope — an async (sub)expression inside a callback
|
|
102
102
|
(`items.map(x => fetchName(x))`, `items.map(async x => await load(x))`) must NOT be hoisted
|
|
103
103
|
to a top-level cell: its parameters/closure vars would become free identifiers, and it
|
|
104
|
-
would run once instead of per row. Stop at the boundary, mirroring
|
|
105
|
-
`
|
|
104
|
+
would run once instead of per row. Stop at the boundary, mirroring the shared
|
|
105
|
+
`hasTopLevelAwait` walk. */
|
|
106
106
|
if (
|
|
107
107
|
ts.isArrowFunction(node) ||
|
|
108
108
|
ts.isFunctionExpression(node) ||
|
|
@@ -27,6 +27,9 @@ export function lowerContext(
|
|
|
27
27
|
computedNames: ReadonlySet<string> = new Set(),
|
|
28
28
|
/* `linked` / async `computed` names, read through `$$readCell(name)`. */
|
|
29
29
|
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(),
|
|
30
33
|
) {
|
|
31
34
|
/* The typed branch-local shadow stack: one auto-popping value owning both kinds.
|
|
32
35
|
`derived` names deref to `.value` like a `computed` (block value params the client
|
|
@@ -51,6 +54,7 @@ export function lowerContext(
|
|
|
51
54
|
new Set(scope.names('derived')),
|
|
52
55
|
new Set(scope.names('plain')),
|
|
53
56
|
cellReadNames,
|
|
57
|
+
blockingCellNames,
|
|
54
58
|
),
|
|
55
59
|
docAccessTransformer('$$model'),
|
|
56
60
|
])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { escapeKey } from '../runtime/escapeKey.ts'
|
|
3
|
+
import { COMPOUND_ASSIGNMENT_OPERATORS } from './COMPOUND_ASSIGNMENT_OPERATORS.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
The linchpin compiler pass. Rewrites idiomatic data access on a reactive document
|
|
@@ -30,9 +31,6 @@ used as an index lowers too. Exposed as `docAccessTransformer` (a
|
|
|
30
31
|
/* A path segment is either a literal key or a runtime expression (a dynamic index). */
|
|
31
32
|
type Segment = { kind: 'literal'; value: string } | { kind: 'expression'; node: ts.Expression }
|
|
32
33
|
|
|
33
|
-
/* Maps a compound-assignment operator to its plain binary counterpart. Logical
|
|
34
|
-
assignments (`||=`/`&&=`/`??=`) lower to an unconditional replace of the
|
|
35
|
-
combined value — consistent with how `+=` lowers (the patch always writes). */
|
|
36
34
|
/* Methods that mutate a doc-rooted container in place — Array, Map and Set (the three
|
|
37
35
|
mutable containers the doc codec serializes). Called on a doc value these can't lower to
|
|
38
36
|
a bare `readCall` (which would mutate the live tree by reference and never emit a patch —
|
|
@@ -57,16 +55,6 @@ const MUTATING_CONTAINER_METHODS = new Set([
|
|
|
57
55
|
'clear',
|
|
58
56
|
])
|
|
59
57
|
|
|
60
|
-
const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
61
|
-
[ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken],
|
|
62
|
-
[ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken],
|
|
63
|
-
[ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken],
|
|
64
|
-
[ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken],
|
|
65
|
-
[ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken],
|
|
66
|
-
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken],
|
|
67
|
-
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
68
|
-
])
|
|
69
|
-
|
|
70
58
|
export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceFile> {
|
|
71
59
|
return (context) => (root) => {
|
|
72
60
|
/* Collects the path of an access chain rooted at `docName`, visiting any
|
|
@@ -114,7 +102,7 @@ export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.
|
|
|
114
102
|
ts.visitNode(node.right, visit) as ts.Expression,
|
|
115
103
|
])
|
|
116
104
|
}
|
|
117
|
-
const binary =
|
|
105
|
+
const binary = COMPOUND_ASSIGNMENT_OPERATORS.get(operator)
|
|
118
106
|
if (binary) {
|
|
119
107
|
const next = ts.factory.createBinaryExpression(
|
|
120
108
|
docCall(docName, 'read', [buildPath(segments)]),
|
|
@@ -91,10 +91,19 @@ export function lowerScript(
|
|
|
91
91
|
derivedNames: Set<string>
|
|
92
92
|
computedNames: Set<string>
|
|
93
93
|
cellReadNames: Set<string>
|
|
94
|
+
/* Template-injected + script-level `await` cells (ADR-0042), read via `$$readCellBlocking`. */
|
|
95
|
+
blockingCellNames: Set<string>
|
|
94
96
|
droppedReactiveImports: Set<string>
|
|
95
97
|
} {
|
|
96
98
|
const source = ts.createSourceFile('component.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
97
|
-
const {
|
|
99
|
+
const {
|
|
100
|
+
transformer,
|
|
101
|
+
stateNames,
|
|
102
|
+
derivedNames,
|
|
103
|
+
computedNames,
|
|
104
|
+
cellReadNames,
|
|
105
|
+
blockingCellNames: allBlockingNames,
|
|
106
|
+
} = desugarSignals(
|
|
98
107
|
source,
|
|
99
108
|
injectedCellNames,
|
|
100
109
|
blockingCellNames,
|
|
@@ -117,9 +126,14 @@ export function lowerScript(
|
|
|
117
126
|
...computedNames,
|
|
118
127
|
...cellReadNames,
|
|
119
128
|
])
|
|
129
|
+
/* `wrapReactionCellSources` only folds `watch(cell, …)` calls, so it's a pure
|
|
130
|
+
identity walk when the script imports no `watch` (the common case) — skip the
|
|
131
|
+
extra full-tree pass entirely then. */
|
|
132
|
+
const reactionTransforms =
|
|
133
|
+
watchLocalNames.size > 0 ? [wrapReactionCellSources(cellNames, watchLocalNames)] : []
|
|
120
134
|
const result = ts.transform(source, [
|
|
121
135
|
transformer,
|
|
122
|
-
|
|
136
|
+
...reactionTransforms,
|
|
123
137
|
signalRefsTransformer(
|
|
124
138
|
stateNames,
|
|
125
139
|
derivedNames,
|
|
@@ -180,6 +194,7 @@ export function lowerScript(
|
|
|
180
194
|
derivedNames,
|
|
181
195
|
computedNames,
|
|
182
196
|
cellReadNames,
|
|
197
|
+
blockingCellNames: allBlockingNames,
|
|
183
198
|
droppedReactiveImports,
|
|
184
199
|
}
|
|
185
200
|
}
|