@abide/abide 0.46.0 → 0.48.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 +370 -320
- package/CHANGELOG.md +94 -0
- package/README.md +203 -168
- package/package.json +12 -8
- package/src/abideLsp.ts +11 -12
- package/src/abideResolverPlugin.ts +9 -4
- package/src/lib/bundle/disconnected.abide +3 -0
- package/src/lib/cli/printCommandHelp.ts +43 -0
- package/src/lib/cli/printSessionHelp.ts +2 -1
- package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
- package/src/lib/cli/runCli.ts +2 -1
- package/src/lib/mcp/buildPrompts.ts +25 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
- package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
- package/src/lib/mcp/mcpSurface.ts +13 -252
- package/src/lib/mcp/mcpTools.ts +187 -0
- package/src/lib/mcp/renderPrompt.ts +25 -0
- package/src/lib/mcp/types/McpSurface.ts +15 -0
- package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
- package/src/lib/mcp/types/PromptMessage.ts +2 -0
- package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
- package/src/lib/mcp/types/ToolResult.ts +2 -0
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/jsonl.ts +2 -2
- package/src/lib/server/rpc/defineRpc.ts +5 -0
- package/src/lib/server/runtime/buildCacheSnapshot.ts +6 -6
- package/src/lib/server/runtime/containsTraversal.ts +21 -17
- package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
- package/src/lib/server/runtime/createServer.ts +101 -71
- package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
- package/src/lib/server/runtime/finalizeResponse.ts +32 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +4 -4
- package/src/lib/server/runtime/types/InspectorContext.ts +1 -1
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
- package/src/lib/server/sockets/defineSocket.ts +26 -7
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
- package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
- package/src/lib/server/sse.ts +2 -2
- package/src/lib/shared/DEFER.ts +8 -0
- package/src/lib/shared/activeCacheStore.ts +2 -2
- package/src/lib/shared/activePage.ts +2 -2
- package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
- package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
- package/src/lib/shared/basePath.ts +2 -2
- package/src/lib/shared/baseSlot.ts +6 -5
- package/src/lib/shared/bodyValueForKind.ts +1 -2
- package/src/lib/shared/buildSocketOverChannel.ts +40 -4
- package/src/lib/shared/cache.ts +522 -117
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/shared/cacheStores.ts +3 -3
- package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
- package/src/lib/shared/createRemoteFunction.ts +58 -9
- package/src/lib/shared/createResolverSlot.ts +24 -31
- package/src/lib/shared/createSocketSubRegistry.ts +2 -2
- package/src/lib/shared/decodeRefJson.ts +4 -2
- package/src/lib/shared/decodeResponse.ts +8 -6
- package/src/lib/shared/done.ts +14 -0
- package/src/lib/shared/encodeRefJson.ts +4 -2
- package/src/lib/shared/hydratingSlot.ts +12 -0
- package/src/lib/shared/isLayoutFile.ts +12 -0
- package/src/lib/shared/keyForRemoteCall.ts +2 -1
- package/src/lib/shared/keyPrefixForRemote.ts +12 -0
- package/src/lib/shared/matchRoute.ts +175 -0
- package/src/lib/shared/normalizePathname.ts +11 -0
- package/src/lib/shared/pageSlot.ts +14 -5
- package/src/lib/shared/pageUrlForFile.ts +3 -3
- package/src/lib/shared/parseRouteSegments.ts +16 -7
- package/src/lib/shared/patch.ts +41 -0
- package/src/lib/shared/peek.ts +35 -0
- package/src/lib/shared/prepareRemoteExport.ts +40 -0
- package/src/lib/shared/prepareRpcModule.ts +98 -16
- package/src/lib/shared/prepareSocketModule.ts +11 -15
- package/src/lib/shared/queryStringFromArgs.ts +11 -0
- package/src/lib/shared/reachable.ts +102 -0
- package/src/lib/shared/refresh.ts +22 -0
- package/src/lib/shared/requestScopeSlot.ts +10 -7
- package/src/lib/shared/routeParamsShape.ts +9 -9
- package/src/lib/shared/rpcErrorRegistry.ts +69 -0
- package/src/lib/shared/selectorPrefix.ts +3 -10
- package/src/lib/shared/setOwnProperty.ts +19 -0
- package/src/lib/shared/sharedCacheStore.ts +14 -0
- package/src/lib/shared/sharedCacheStoreSlot.ts +10 -0
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/subscribableProbes.ts +111 -0
- package/src/lib/shared/tailProbeSlot.ts +8 -1
- package/src/lib/shared/types/CacheEntry.ts +9 -15
- package/src/lib/shared/types/CacheOptions.ts +23 -11
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/CacheStats.ts +1 -1
- package/src/lib/shared/types/RemoteCallable.ts +25 -7
- package/src/lib/shared/types/RemoteFunction.ts +48 -13
- package/src/lib/shared/types/ResolverSlot.ts +7 -4
- package/src/lib/shared/types/RpcError.ts +26 -0
- package/src/lib/shared/types/SmartReadOptions.ts +36 -0
- package/src/lib/shared/types/Socket.ts +54 -0
- package/src/lib/shared/types/SsrBootState.ts +27 -0
- package/src/lib/shared/types/SsrPayload.ts +21 -0
- package/src/lib/shared/types/Subscribable.ts +13 -13
- package/src/lib/shared/types/TailHooks.ts +2 -1
- package/src/lib/shared/url.ts +29 -14
- package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +8 -8
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
- package/src/lib/ui/compile/analyzeComponent.ts +28 -6
- package/src/lib/ui/compile/compileModule.ts +137 -11
- package/src/lib/ui/compile/compileShadow.ts +101 -84
- package/src/lib/ui/compile/desugarSignals.ts +120 -107
- package/src/lib/ui/compile/generateBuild.ts +48 -16
- package/src/lib/ui/compile/generateSSR.ts +117 -14
- package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -4
- package/src/lib/ui/compile/lowerScript.ts +72 -5
- package/src/lib/ui/compile/parseTemplate.ts +1 -14
- package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
- package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
- package/src/lib/ui/compile/signalCallee.ts +24 -0
- package/src/lib/ui/compile/stripEffects.ts +14 -11
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
- package/src/lib/ui/dom/appendStatic.ts +9 -1
- package/src/lib/ui/dom/appendText.ts +15 -3
- package/src/lib/ui/dom/assertClaimedText.ts +20 -0
- package/src/lib/ui/dom/awaitBlock.ts +19 -83
- package/src/lib/ui/dom/bindSelectValue.ts +48 -0
- package/src/lib/ui/dom/each.ts +12 -1
- package/src/lib/ui/dom/hydrate.ts +10 -0
- package/src/lib/ui/dom/mountChild.ts +2 -5
- package/src/lib/ui/dom/mountRange.ts +0 -75
- package/src/lib/ui/effect.ts +4 -3
- package/src/lib/ui/installHotBridge.ts +4 -2
- package/src/lib/ui/remoteProxy.ts +18 -2
- package/src/lib/ui/renderToStream.ts +9 -13
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/router.ts +21 -2
- package/src/lib/ui/runtime/RESUME.ts +0 -6
- package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
- package/src/lib/ui/scope.ts +6 -3
- package/src/lib/ui/seedBootState.ts +53 -0
- package/src/lib/ui/socketChannel.ts +2 -2
- package/src/lib/ui/socketProxy.ts +9 -3
- package/src/lib/ui/startClient.ts +17 -31
- package/src/lib/ui/state.ts +44 -13
- package/src/lib/ui/sync.ts +11 -5
- package/src/lib/ui/tryEncodeResume.ts +2 -5
- package/src/lib/ui/types/Scope.ts +14 -10
- package/src/lib/ui/watch.ts +140 -0
- package/src/serverEntry.ts +13 -11
- package/template/CLAUDE.md +1 -1
- package/template/package.json +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +2 -2
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/reachable.ts +0 -45
- package/src/lib/server/sockets/types/Socket.ts +0 -23
- package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
- package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
- package/src/lib/shared/baseResolver.ts +0 -10
- package/src/lib/shared/cacheKeyOf.ts +0 -7
- package/src/lib/shared/cacheKeyStore.ts +0 -8
- package/src/lib/shared/cacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStore.ts +0 -15
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreSlot.ts +0 -9
- package/src/lib/shared/pageResolver.ts +0 -16
- package/src/lib/shared/recordCacheKey.ts +0 -6
- package/src/lib/shared/requestScopeResolver.ts +0 -12
- package/src/lib/shared/setBaseResolver.ts +0 -4
- package/src/lib/shared/setCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setPageResolver.ts +0 -4
- package/src/lib/shared/setRequestScopeResolver.ts +0 -4
- package/src/lib/shared/toBunRoutePattern.ts +0 -28
- package/src/lib/shared/types/TailOptions.ts +0 -10
- package/src/lib/ui/deferResume.ts +0 -36
- package/src/lib/ui/dom/firstElementBetween.ts +0 -14
- package/src/lib/ui/matchRoute.ts +0 -106
- package/src/lib/ui/runtime/scheduleWake.ts +0 -28
- package/src/lib/ui/runtime/whenIdle.ts +0 -21
- package/src/lib/ui/runtime/whenVisible.ts +0 -105
- package/src/lib/ui/tail.ts +0 -324
- /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
- /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
|
@@ -23,6 +23,7 @@ import { skeletonContext } from './skeletonContext.ts'
|
|
|
23
23
|
import { snippetPlan } from './snippetPlan.ts'
|
|
24
24
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
25
25
|
import { staticAttr } from './staticAttr.ts'
|
|
26
|
+
import { staticAttrValue } from './staticAttrValue.ts'
|
|
26
27
|
import { staticTextPart } from './staticTextPart.ts'
|
|
27
28
|
import { stripEffects } from './stripEffects.ts'
|
|
28
29
|
import { switchPlan } from './switchPlan.ts'
|
|
@@ -70,6 +71,12 @@ export function generateSSR(
|
|
|
70
71
|
allocated separately at runtime via `$ctx.next++`. */
|
|
71
72
|
const nextVar = makeVarNamer()
|
|
72
73
|
|
|
74
|
+
/* The enclosing `<select bind:value>`s, innermost last: each carries the JS var holding
|
|
75
|
+
its bound value and whether it's a `multiple` (array) select, so an `<option>` rendered
|
|
76
|
+
within can emit `selected` by comparing its own value against the bound one. Pushed when
|
|
77
|
+
a bound select opens, popped after its children. */
|
|
78
|
+
const selectBinds: { variable: string; multiple: boolean }[] = []
|
|
79
|
+
|
|
73
80
|
/* The shared signal→`model` lowering + branch-scoped nested-script deref scope. */
|
|
74
81
|
const {
|
|
75
82
|
expression: lowerExpression,
|
|
@@ -92,6 +99,47 @@ export function generateSSR(
|
|
|
92
99
|
return `${target}.push(${JSON.stringify(literal)});\n`
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/* The JS expression for an `<option>`'s value, to compare against an enclosing bound
|
|
103
|
+
`<select>`: the `value` attribute (static/expression/interpolated), else the option's
|
|
104
|
+
static text content (the browser's own fallback, whitespace-trimmed). Returns undefined
|
|
105
|
+
when the value can't be known at compile time (a dynamic-text option with no `value`
|
|
106
|
+
attr) — the client selects it on hydrate. */
|
|
107
|
+
function optionValueForSSR(
|
|
108
|
+
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
109
|
+
): string | undefined {
|
|
110
|
+
const valueAttr = node.attrs.find(
|
|
111
|
+
(attr) =>
|
|
112
|
+
(attr.kind === 'static' ||
|
|
113
|
+
attr.kind === 'expression' ||
|
|
114
|
+
attr.kind === 'interpolated') &&
|
|
115
|
+
attr.name === 'value',
|
|
116
|
+
)
|
|
117
|
+
if (valueAttr !== undefined) {
|
|
118
|
+
if (valueAttr.kind === 'static') {
|
|
119
|
+
return JSON.stringify(valueAttr.value)
|
|
120
|
+
}
|
|
121
|
+
if (valueAttr.kind === 'expression') {
|
|
122
|
+
return lowerExpression(valueAttr.code)
|
|
123
|
+
}
|
|
124
|
+
if (valueAttr.kind === 'interpolated') {
|
|
125
|
+
return lowerExpression(interpolatedTemplateLiteral(valueAttr.parts))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let staticText = ''
|
|
129
|
+
for (const child of node.children) {
|
|
130
|
+
if (child.kind !== 'text') {
|
|
131
|
+
return undefined
|
|
132
|
+
}
|
|
133
|
+
for (const part of child.parts) {
|
|
134
|
+
if (part.kind !== 'static') {
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
staticText += part.value
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify(staticText.trim())
|
|
141
|
+
}
|
|
142
|
+
|
|
95
143
|
function generateInto(children: TemplateNode[], target: string): string {
|
|
96
144
|
return children.map((child) => generate(child, target)).join('')
|
|
97
145
|
}
|
|
@@ -110,8 +158,18 @@ export function generateSSR(
|
|
|
110
158
|
never matches (`$` is a non-word char, so there is no word boundary before it), which
|
|
111
159
|
would silently miss every `$row(...)` call. A negative lookbehind for word-or-`$`
|
|
112
160
|
matches the same call sites as `\b` for word-leading names while also catching them. */
|
|
113
|
-
|
|
114
|
-
|
|
161
|
+
/* Memoised per name: the pattern is a pure function of the name but tested across the
|
|
162
|
+
fixpoint loop below (per snippet × per text part × per iteration), so recompiling the
|
|
163
|
+
RegExp each test — far costlier than the test itself — dominated the snippet scan. */
|
|
164
|
+
const callPatternCache = new Map<string, RegExp>()
|
|
165
|
+
const callPattern = (name: string): RegExp => {
|
|
166
|
+
let pattern = callPatternCache.get(name)
|
|
167
|
+
if (pattern === undefined) {
|
|
168
|
+
pattern = new RegExp(`(?<![$\\w])${escapeRegex(name)}\\s*\\(`)
|
|
169
|
+
callPatternCache.set(name, pattern)
|
|
170
|
+
}
|
|
171
|
+
return pattern
|
|
172
|
+
}
|
|
115
173
|
/* A subtree call to any of `names` from a TEXT interpolation (`name()` / `name(args)`). */
|
|
116
174
|
const subtreeCalls = (children: TemplateNode[], names: ReadonlySet<string>): boolean =>
|
|
117
175
|
children.some((child) => {
|
|
@@ -167,8 +225,18 @@ export function generateSSR(
|
|
|
167
225
|
}
|
|
168
226
|
/* A text-part expression that calls an async snippet, so its value is `await`ed before
|
|
169
227
|
`$text`. */
|
|
170
|
-
const callsAsyncSnippet = (code: string): boolean =>
|
|
171
|
-
|
|
228
|
+
const callsAsyncSnippet = (code: string): boolean => {
|
|
229
|
+
// The common no-async-snippet component pays zero regex work per text part.
|
|
230
|
+
if (asyncSnippets.size === 0) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
for (const name of asyncSnippets) {
|
|
234
|
+
if (callPattern(name).test(code)) {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
172
240
|
|
|
173
241
|
/* Per-node skeleton position, computed once. Both back-ends read this single source of
|
|
174
242
|
truth so their `<!--a-->` anchor placement cannot drift — the fresh-context boundaries
|
|
@@ -443,6 +511,14 @@ export function generateSSR(
|
|
|
443
511
|
/* A boolean property — its mere presence means checked, so emit the
|
|
444
512
|
attribute only when truthy (a string `checked="false"` still checks). */
|
|
445
513
|
code += `${target}.push((${bindRead(attr.code)}) ? ' checked' : '');\n`
|
|
514
|
+
} else if (attr.kind === 'bind' && attr.property === 'open') {
|
|
515
|
+
/* `<details open>` — `open` is a boolean attribute, so `open="false"` would
|
|
516
|
+
still render open. Emit the bare attribute only when truthy, like checked. */
|
|
517
|
+
code += `${target}.push((${bindRead(attr.code)}) ? ' open' : '');\n`
|
|
518
|
+
} else if (attr.kind === 'bind' && attr.property === 'value' && node.tag === 'select') {
|
|
519
|
+
/* `<select bind:value>` selects via `selected` on the matching option (a
|
|
520
|
+
`value="…"` on the select is ignored by browsers), wired below. Emit nothing
|
|
521
|
+
here. */
|
|
446
522
|
} else if (attr.kind === 'bind') {
|
|
447
523
|
code += `${target}.push(${JSON.stringify(` ${attr.property}="`)} + $esc(${bindRead(attr.code)}) + '"');\n`
|
|
448
524
|
}
|
|
@@ -455,7 +531,33 @@ export function generateSSR(
|
|
|
455
531
|
if (merge.mergeStyle) {
|
|
456
532
|
code += `${target}.push(' style="' + $esc([${merge.styleParts.join(', ')}].filter(Boolean).join(';')) + '"');\n`
|
|
457
533
|
}
|
|
534
|
+
/* An `<option>` inside a bound `<select>`: emit `selected` when its value equals the
|
|
535
|
+
bound value (single) or is a member of it (multiple). */
|
|
536
|
+
if (node.tag === 'option' && selectBinds.length > 0) {
|
|
537
|
+
const optionValue = optionValueForSSR(node)
|
|
538
|
+
const bind = selectBinds[selectBinds.length - 1]
|
|
539
|
+
if (optionValue !== undefined && bind !== undefined) {
|
|
540
|
+
const present = bind.multiple
|
|
541
|
+
? `Array.isArray(${bind.variable}) && ${bind.variable}.includes(${optionValue})`
|
|
542
|
+
: `(${optionValue}) === (${bind.variable})`
|
|
543
|
+
code += `${target}.push((${present}) ? ' selected' : '');\n`
|
|
544
|
+
}
|
|
545
|
+
}
|
|
458
546
|
code += push(target, '>')
|
|
547
|
+
/* A bound `<select>` publishes its current value to a local so the options rendered
|
|
548
|
+
as its children can compare against it; popped once past those children. */
|
|
549
|
+
const selectValueBind =
|
|
550
|
+
node.tag === 'select'
|
|
551
|
+
? node.attrs.find((attr) => attr.kind === 'bind' && attr.property === 'value')
|
|
552
|
+
: undefined
|
|
553
|
+
if (selectValueBind !== undefined && selectValueBind.kind === 'bind') {
|
|
554
|
+
const variable = nextVar('$sel')
|
|
555
|
+
code += `const ${variable} = ${bindRead(selectValueBind.code)};\n`
|
|
556
|
+
selectBinds.push({
|
|
557
|
+
variable,
|
|
558
|
+
multiple: staticAttrValue(node, 'multiple') !== undefined,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
459
561
|
if (!plan.isVoid) {
|
|
460
562
|
/* Each child's skeleton position (whether its reactive text interleaves into an
|
|
461
563
|
anchor, whether a nested block anchors) is already recorded by `skeletonContext`
|
|
@@ -464,6 +566,9 @@ export function generateSSR(
|
|
|
464
566
|
code += withNestedScripts(node.children, () => generateInto(node.children, target))
|
|
465
567
|
code += push(target, `</${node.tag}>`)
|
|
466
568
|
}
|
|
569
|
+
if (selectValueBind !== undefined) {
|
|
570
|
+
selectBinds.pop()
|
|
571
|
+
}
|
|
467
572
|
return code
|
|
468
573
|
}
|
|
469
574
|
|
|
@@ -527,15 +632,10 @@ export function generateSSR(
|
|
|
527
632
|
`withBindings` shadow models, so a reference to the binding reads the plain
|
|
528
633
|
local rather than the (unresolved) component signal it shadows. */
|
|
529
634
|
const resolved = nextVar('$av')
|
|
530
|
-
/* Keep the promise object: a cache() read tagged it with its store key, which
|
|
531
|
-
`$$deferResume` reads to defer a cache-backed value (ship a `{defer,key}` marker +
|
|
532
|
-
lazy seed) instead of inlining it — the blocking form's contract. */
|
|
533
|
-
const promiseVar = nextVar('$ap')
|
|
534
635
|
let code = `const ${id} = $ctx.next++;\n`
|
|
535
636
|
code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
|
|
536
637
|
code += `try {\n`
|
|
537
|
-
code += `const ${
|
|
538
|
-
code += `const ${resolved} = await ${promiseVar};\n`
|
|
638
|
+
code += `const ${resolved} = await (${lowerExpression(node.promise)});\n`
|
|
539
639
|
code += `{\n`
|
|
540
640
|
code += `const ${plan.resolvedAs} = ${resolved};\n`
|
|
541
641
|
code += withBindings(withShadow, plan.resolvedBindings, ssrBindingKind, () =>
|
|
@@ -545,7 +645,12 @@ export function generateSSR(
|
|
|
545
645
|
shadow — matching the catch branch below, so a finally expression naming the
|
|
546
646
|
same identifier as the `then` binding reads the component signal, not the local. */
|
|
547
647
|
code += branchContent(plan.finallyChildren, target)
|
|
548
|
-
|
|
648
|
+
/* Seed the resolved value into the resume manifest so hydration adopts the server
|
|
649
|
+
branch warm (no round-trip) and wires it live on the first frame. Seed the
|
|
650
|
+
resolved temp, NOT `plan.resolvedAs` — the latter is the author's binding
|
|
651
|
+
PATTERN (e.g. `{name}` or `{name = 'anon'}`), which as an expression rebuilds a
|
|
652
|
+
partial object or is a CoverInitializedName syntax error. */
|
|
653
|
+
code += `$resume[${id}] = { ok: true, value: ${resolved} };\n`
|
|
549
654
|
code += `}\n`
|
|
550
655
|
if (plan.surfaceRejection) {
|
|
551
656
|
/* No catch/finally → let the rejection surface instead of an empty branch. */
|
|
@@ -569,9 +674,7 @@ export function generateSSR(
|
|
|
569
674
|
`await` block inside the branch composes. `finally` appends after the outcome,
|
|
570
675
|
matching the client's concatenated node range. Neither catch nor finally → omit
|
|
571
676
|
`catch` so a rejection surfaces (renderToStream re-throws); a finally-only block
|
|
572
|
-
keeps a catch renderer that renders just finally.
|
|
573
|
-
DEFERRED at settle time (renderToStream ships a `{defer,key}` marker + lazy body, the
|
|
574
|
-
client adopts the streamed branch inert) — the machinery here is unchanged. */
|
|
677
|
+
keeps a catch renderer that renders just finally. */
|
|
575
678
|
function generateStreamingAwait(
|
|
576
679
|
node: Extract<TemplateNode, { kind: 'await' }>,
|
|
577
680
|
target: string,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { escapeRegex } from '../../shared/escapeRegex.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A RegExp that matches `name` used as a whole identifier in a body of code.
|
|
5
|
+
Boundaries are `(?<![$\w]) … (?![$\w])`, NOT `\b`: `\b` never fires before a
|
|
6
|
+
`$`-leading identifier (`$` is a non-word char, so there's no word boundary
|
|
7
|
+
before it), so `\b$e\b` silently matches nothing — the exact miss that made an
|
|
8
|
+
aliased reactive import (`import { effect as $e }`) read as dead. The name is
|
|
9
|
+
regex-escaped so a `$` or other metachar in it is literal, not an anchor.
|
|
10
|
+
*/
|
|
11
|
+
export function identifierReferencePattern(name: string): RegExp {
|
|
12
|
+
return new RegExp(`(?<![$\\w])${escapeRegex(name)}(?![$\\w])`)
|
|
13
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { createShadowScope } from './createShadowScope.ts'
|
|
3
3
|
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
4
|
-
import { nestedBindingNames } from './prepareNestedScript.ts'
|
|
4
|
+
import { nestedBindingNames, nestedPlainLocalNames } from './prepareNestedScript.ts'
|
|
5
5
|
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
6
6
|
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
7
7
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
@@ -105,12 +105,18 @@ export function lowerContext(
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/* Adds any `<script>` children's binding names to the deref scope (so the script
|
|
108
|
-
bodies and the branch's markup auto-deref them) for the duration of `body`.
|
|
108
|
+
bodies and the branch's markup auto-deref them) for the duration of `body`. Reactive
|
|
109
|
+
bindings register as `derived` `.value` cells; the script's PLAIN top-level locals
|
|
110
|
+
register as `plain` bare locals so a local that shares a name with a component signal
|
|
111
|
+
shadows it (else the reference lowers to `$$model.read(...)`, ignoring the local). */
|
|
109
112
|
function withNestedScripts<T>(children: TemplateNode[], body: () => T): T {
|
|
110
|
-
const
|
|
113
|
+
const derived = children.flatMap((child) =>
|
|
111
114
|
child.kind === 'script' ? [...nestedBindingNames(child.code)] : [],
|
|
112
115
|
)
|
|
113
|
-
|
|
116
|
+
const plain = children.flatMap((child) =>
|
|
117
|
+
child.kind === 'script' ? [...nestedPlainLocalNames(child.code)] : [],
|
|
118
|
+
)
|
|
119
|
+
return scope.withShadow(derived, 'derived', () => scope.withShadow(plain, 'plain', body))
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
return {
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
+
import { ABIDE_PACKAGE_NAME } from '../../shared/ABIDE_PACKAGE_NAME.ts'
|
|
2
3
|
import { assertTranspiles } from './assertTranspiles.ts'
|
|
3
4
|
import { desugarSignals } from './desugarSignals.ts'
|
|
5
|
+
import { identifierReferencePattern } from './identifierReferencePattern.ts'
|
|
4
6
|
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
5
7
|
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
6
8
|
import { stripEffectsTransformer } from './stripEffects.ts'
|
|
7
9
|
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
8
10
|
|
|
11
|
+
/* The `abide/ui/*` modules the reactive surface is imported from. An author's import of
|
|
12
|
+
one is compiler-recognised and lowered, so its binding is often fully consumed — a plain
|
|
13
|
+
`state(...)` becomes `$$model`/`$$scope` with no `state` reference left. Such a dead
|
|
14
|
+
import is dropped from the emitted module (see `deadReactiveImport`) so the output has no
|
|
15
|
+
spurious static `@abide/ui` dependency — load-bearing for hot modules, which forbid one. */
|
|
16
|
+
const REACTIVE_IMPORT_SPECIFIERS = new Set([
|
|
17
|
+
`${ABIDE_PACKAGE_NAME}/ui/state`,
|
|
18
|
+
`${ABIDE_PACKAGE_NAME}/ui/effect`,
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/* True when `statement` imports the reactive surface and none of its local bindings survive
|
|
22
|
+
into the lowered output (`used` = body + ssr body) — so emitting it would be a dead,
|
|
23
|
+
spurious runtime import. A binding that DOES survive (`state.share`, a bare `effect(...)`)
|
|
24
|
+
keeps the import. */
|
|
25
|
+
function deadReactiveImport(statement: ts.ImportDeclaration, used: string): boolean {
|
|
26
|
+
if (
|
|
27
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
28
|
+
!REACTIVE_IMPORT_SPECIFIERS.has(statement.moduleSpecifier.text)
|
|
29
|
+
) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
const named = statement.importClause?.namedBindings
|
|
33
|
+
if (named === undefined || !ts.isNamedImports(named)) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return named.elements.every(
|
|
37
|
+
(element) => !identifierReferencePattern(element.name.text).test(used),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
/*
|
|
10
42
|
The component-script lowering, done in ONE parse. The script is parsed once, then
|
|
11
43
|
`desugarSignals` (signal declarations → `model` slots / `scope().derive`), reference
|
|
@@ -21,13 +53,22 @@ position corrupts the script (the failure mode the syntax fuzz corpus also guard
|
|
|
21
53
|
surfaces here as a located compile error instead of shipping a broken bundle.
|
|
22
54
|
*/
|
|
23
55
|
|
|
24
|
-
export function lowerScript(
|
|
56
|
+
export function lowerScript(
|
|
57
|
+
scriptBody: string,
|
|
58
|
+
/* Reactive-surface identifiers referenced OUTSIDE this script — the nested branch
|
|
59
|
+
`<script>`s, which keep their `state.computed(...)` calls literal and so still need
|
|
60
|
+
the module-level import even after the leading script's own `state(...)` all
|
|
61
|
+
desugared away. Folded into the dead-import usage check so a live import isn't dropped
|
|
62
|
+
(→ `ReferenceError: state is not defined` in the branch). Empty for a nested script. */
|
|
63
|
+
externalUsage = '',
|
|
64
|
+
): {
|
|
25
65
|
body: string
|
|
26
66
|
imports: string
|
|
27
67
|
ssrBody: string
|
|
28
68
|
stateNames: Set<string>
|
|
29
69
|
derivedNames: Set<string>
|
|
30
70
|
computedNames: Set<string>
|
|
71
|
+
droppedReactiveImports: Set<string>
|
|
31
72
|
} {
|
|
32
73
|
const source = ts.createSourceFile('component.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
33
74
|
const { transformer, stateNames, derivedNames, computedNames } = desugarSignals(source)
|
|
@@ -43,9 +84,6 @@ export function lowerScript(scriptBody: string): {
|
|
|
43
84
|
const bodyStatements = transformed.statements.filter(
|
|
44
85
|
(statement) => !ts.isImportDeclaration(statement),
|
|
45
86
|
)
|
|
46
|
-
const imports = importStatements
|
|
47
|
-
.map((statement) => TS_PRINTER.printNode(ts.EmitHint.Unspecified, statement, transformed))
|
|
48
|
-
.join('\n')
|
|
49
87
|
const bodyFile = ts.factory.updateSourceFile(transformed, bodyStatements)
|
|
50
88
|
const body = TS_PRINTER.printFile(bodyFile).trim()
|
|
51
89
|
/* The SSR variant strips client-only `effect(...)` calls — run over the SAME lowered
|
|
@@ -55,10 +93,39 @@ export function lowerScript(scriptBody: string): {
|
|
|
55
93
|
const ssrBody = TS_PRINTER.printFile(ssrResult.transformed[0] as ts.SourceFile).trim()
|
|
56
94
|
ssrResult.dispose()
|
|
57
95
|
result.dispose()
|
|
96
|
+
/* Drop reactive-surface imports fully consumed by lowering — keeping them would leave a
|
|
97
|
+
dead, spurious `@abide/ui` runtime dependency (checked against both back-ends' output). */
|
|
98
|
+
const used = `${body}\n${ssrBody}\n${externalUsage}`
|
|
99
|
+
const droppedReactiveImports = new Set<string>()
|
|
100
|
+
const imports = importStatements
|
|
101
|
+
.filter((statement) => {
|
|
102
|
+
if (!deadReactiveImport(statement, used)) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
/* Record the local names of a dropped reactive import so the module wrapper can
|
|
106
|
+
independently confirm the drop stranded no live reference. */
|
|
107
|
+
const named = statement.importClause?.namedBindings
|
|
108
|
+
if (named !== undefined && ts.isNamedImports(named)) {
|
|
109
|
+
for (const element of named.elements) {
|
|
110
|
+
droppedReactiveImports.add(element.name.text)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false
|
|
114
|
+
})
|
|
115
|
+
.map((statement) => TS_PRINTER.printNode(ts.EmitHint.Unspecified, statement, transformed))
|
|
116
|
+
.join('\n')
|
|
58
117
|
|
|
59
118
|
assertTranspiles(
|
|
60
119
|
[imports, body].filter((part) => part !== '').join('\n'),
|
|
61
120
|
'component script lowering',
|
|
62
121
|
)
|
|
63
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
body,
|
|
124
|
+
imports,
|
|
125
|
+
ssrBody,
|
|
126
|
+
stateNames,
|
|
127
|
+
derivedNames,
|
|
128
|
+
computedNames,
|
|
129
|
+
droppedReactiveImports,
|
|
130
|
+
}
|
|
64
131
|
}
|
|
@@ -640,25 +640,12 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
640
640
|
its children become slot content (rendered where the child puts <slot>). */
|
|
641
641
|
if (UPPERCASE_START.test(tag)) {
|
|
642
642
|
const slotted = selfClosing ? [] : readChildren(tag)
|
|
643
|
-
/* `client:idle` / `client:visible` are island directives (bare) — pull them out of
|
|
644
|
-
the attributes so they never round-trip as props; only the build back-end reads
|
|
645
|
-
the trigger. Any other `client:*` falls through as a prop (a prop-check error). */
|
|
646
|
-
const clientAttr = attrs.find(
|
|
647
|
-
(attr) =>
|
|
648
|
-
attr.kind === 'static' &&
|
|
649
|
-
(attr.name === 'client:idle' || attr.name === 'client:visible'),
|
|
650
|
-
)
|
|
651
|
-
const clientTrigger =
|
|
652
|
-
clientAttr?.kind === 'static'
|
|
653
|
-
? (clientAttr.name.slice('client:'.length) as 'idle' | 'visible')
|
|
654
|
-
: undefined
|
|
655
643
|
return {
|
|
656
644
|
kind: 'component',
|
|
657
645
|
name: tag,
|
|
658
646
|
loc: baseOffset + tagStart,
|
|
659
|
-
props: toProps(attrs
|
|
647
|
+
props: toProps(attrs),
|
|
660
648
|
children: slotted,
|
|
661
|
-
clientTrigger,
|
|
662
649
|
}
|
|
663
650
|
}
|
|
664
651
|
/* `{...expr}` spreads onto a component (its props) or a native element (its
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
-
import {
|
|
2
|
+
import { NESTED_REACTIVE_BINDINGS } from './resolveReactiveExport.ts'
|
|
3
|
+
import { signalCallee } from './signalCallee.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
The signal binding names a `<script>` nested in a control-flow branch declares
|
|
@@ -17,10 +18,14 @@ export function nestedBindingNames(code: string): Set<string> {
|
|
|
17
18
|
continue
|
|
18
19
|
}
|
|
19
20
|
for (const declaration of statement.declarationList.declarations) {
|
|
20
|
-
const callee = signalCallee(declaration)
|
|
21
|
+
const callee = signalCallee(declaration, NESTED_REACTIVE_BINDINGS)
|
|
22
|
+
/* `effect`/`watch` bind no readable value (they return a disposer, not a cell), so
|
|
23
|
+
they never join the deref scope; every other recognised primitive
|
|
24
|
+
(state/linked/computed/props) does. */
|
|
21
25
|
if (
|
|
22
26
|
callee !== undefined &&
|
|
23
|
-
|
|
27
|
+
callee !== 'effect' &&
|
|
28
|
+
callee !== 'watch' &&
|
|
24
29
|
ts.isIdentifier(declaration.name)
|
|
25
30
|
) {
|
|
26
31
|
names.add(declaration.name.text)
|
|
@@ -30,19 +35,28 @@ export function nestedBindingNames(code: string): Set<string> {
|
|
|
30
35
|
return names
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
/* The
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
/* The PLAIN (non-reactive) local names a nested branch `<script>` declares at its top
|
|
39
|
+
level — a `const title = deriveLocal()` that is neither a signal nor an effect/watch.
|
|
40
|
+
These must shadow a same-named component signal so a later reference in the script or
|
|
41
|
+
branch reads the nearer local, not `$$model.read("title")`; registered as `plain`
|
|
42
|
+
shadows (bare locals), the counterpart to `nestedBindingNames`' `derived` cells. */
|
|
43
|
+
export function nestedPlainLocalNames(code: string): Set<string> {
|
|
44
|
+
const source = ts.createSourceFile('nested.ts', code, ts.ScriptTarget.Latest, true)
|
|
45
|
+
const names = new Set<string>()
|
|
46
|
+
for (const statement of source.statements) {
|
|
47
|
+
if (!ts.isVariableStatement(statement)) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
51
|
+
/* Reactive declarations are the `derived` cells `nestedBindingNames` owns; a
|
|
52
|
+
plain identifier declaration with a non-reactive callee (or no call) is a
|
|
53
|
+
local. Skip destructuring patterns — the reactive path does too. */
|
|
54
|
+
const callee = signalCallee(declaration, NESTED_REACTIVE_BINDINGS)
|
|
55
|
+
const isReactive = callee !== undefined && callee !== 'effect' && callee !== 'watch'
|
|
56
|
+
if (!isReactive && ts.isIdentifier(declaration.name)) {
|
|
57
|
+
names.add(declaration.name.text)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
46
60
|
}
|
|
47
|
-
return
|
|
61
|
+
return names
|
|
48
62
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { ABIDE_PACKAGE_NAME } from '../../shared/ABIDE_PACKAGE_NAME.ts'
|
|
3
|
+
|
|
4
|
+
/* The reactive primitive an import binding resolves to — the single recognition
|
|
5
|
+
vocabulary the desugarer, the nested-script scoper, and the shadow consume.
|
|
6
|
+
`linked`/`computed` are reached as members of a `state` import (`state.linked`),
|
|
7
|
+
so they are resolution results but carry no import specifier of their own. */
|
|
8
|
+
export type ReactivePrimitive = 'state' | 'linked' | 'computed' | 'effect' | 'watch' | 'props'
|
|
9
|
+
|
|
10
|
+
/* The `abide/ui/*` specifier each importable reactive primitive is published at,
|
|
11
|
+
mapped to its canonical name. Built from the package name so a rename is one edit.
|
|
12
|
+
`linked`/`computed` are members of `state`, not standalone imports; `props` is
|
|
13
|
+
ambient sugar today (no module) — kept here so a future `abide/ui/props` resolves
|
|
14
|
+
with no code change. */
|
|
15
|
+
const REACTIVE_SPECIFIERS: Record<string, ReactivePrimitive> = {
|
|
16
|
+
[`${ABIDE_PACKAGE_NAME}/ui/state`]: 'state',
|
|
17
|
+
[`${ABIDE_PACKAGE_NAME}/ui/effect`]: 'effect',
|
|
18
|
+
[`${ABIDE_PACKAGE_NAME}/ui/watch`]: 'watch',
|
|
19
|
+
[`${ABIDE_PACKAGE_NAME}/ui/props`]: 'props',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* The reactive import bindings a source file declares: each local binding (alias-safe)
|
|
23
|
+
mapped to its canonical primitive, plus the set of locals bound to `state` (so a
|
|
24
|
+
`state.linked` / `state.computed` member call resolves off the right root). Scans the
|
|
25
|
+
file's `import` statements once — no checker, no `ts.Program`: recognition is purely
|
|
26
|
+
syntactic, correct because abide has no barrels (a primitive is imported directly from
|
|
27
|
+
its `abide/ui/*` module, never a re-export). */
|
|
28
|
+
export type ReactiveImportBindings = {
|
|
29
|
+
direct: Map<string, ReactivePrimitive>
|
|
30
|
+
stateRoots: Set<string>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* The reactive bindings a nested `<template>` <script> resolves against. A nested script
|
|
34
|
+
cannot carry its own import (imports are module-scoped, hoisted off the leading script or
|
|
35
|
+
absent entirely), so it inherits the surface by the canonical names — the one recognition
|
|
36
|
+
site that is name-based because imports structurally cannot reach it. */
|
|
37
|
+
export const NESTED_REACTIVE_BINDINGS: ReactiveImportBindings = {
|
|
38
|
+
direct: new Map<string, ReactivePrimitive>([
|
|
39
|
+
['state', 'state'],
|
|
40
|
+
['effect', 'effect'],
|
|
41
|
+
['watch', 'watch'],
|
|
42
|
+
]),
|
|
43
|
+
stateRoots: new Set(['state']),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function reactiveImportBindings(source: ts.SourceFile): ReactiveImportBindings {
|
|
47
|
+
const direct = new Map<string, ReactivePrimitive>()
|
|
48
|
+
const stateRoots = new Set<string>()
|
|
49
|
+
for (const statement of source.statements) {
|
|
50
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const canonical = REACTIVE_SPECIFIERS[statement.moduleSpecifier.text]
|
|
54
|
+
const named = statement.importClause?.namedBindings
|
|
55
|
+
if (canonical === undefined || named === undefined || !ts.isNamedImports(named)) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
for (const element of named.elements) {
|
|
59
|
+
/* The imported name (`propertyName` when aliased, else `name`) must be the
|
|
60
|
+
module's canonical export; `name` is the local binding codegen resolves. */
|
|
61
|
+
if ((element.propertyName ?? element.name).text !== canonical) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
direct.set(element.name.text, canonical)
|
|
65
|
+
if (canonical === 'state') {
|
|
66
|
+
stateRoots.add(element.name.text)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { direct, stateRoots }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The reactive primitive a call's callee resolves to, or undefined. A bare `props`
|
|
74
|
+
identifier is always the ambient prop reader — the one primitive with no runtime module
|
|
75
|
+
(pure compiler sugar), so it resolves with no import. Every other bare identifier
|
|
76
|
+
resolves through the direct import bindings (alias-safe); a `stateRoot.linked` /
|
|
77
|
+
`.computed` member call resolves off a local bound to `state`. Every other callee is
|
|
78
|
+
undefined (a user's own function, an unrelated member access). */
|
|
79
|
+
export function resolveReactiveExport(
|
|
80
|
+
callee: ts.Expression,
|
|
81
|
+
bindings: ReactiveImportBindings,
|
|
82
|
+
): ReactivePrimitive | undefined {
|
|
83
|
+
if (ts.isIdentifier(callee)) {
|
|
84
|
+
return callee.text === 'props' ? 'props' : bindings.direct.get(callee.text)
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
88
|
+
ts.isIdentifier(callee.expression) &&
|
|
89
|
+
bindings.stateRoots.has(callee.expression.text) &&
|
|
90
|
+
(callee.name.text === 'linked' || callee.name.text === 'computed')
|
|
91
|
+
) {
|
|
92
|
+
return callee.name.text
|
|
93
|
+
}
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import {
|
|
3
|
+
type ReactiveImportBindings,
|
|
4
|
+
type ReactivePrimitive,
|
|
5
|
+
resolveReactiveExport,
|
|
6
|
+
} from './resolveReactiveExport.ts'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
The canonical reactive primitive a `NAME = state(...)` declaration's callee resolves to,
|
|
10
|
+
else undefined. Import-resolution is the SOLE recognition path (alias-safe: an
|
|
11
|
+
`import { state as s }` still resolves `s(...)` to `state`); the legacy member forms are
|
|
12
|
+
not recognised. Shared by the desugar, shadow type-check, and nested-script passes so all
|
|
13
|
+
three agree on what is reactive — a divergence here silently mis-classifies a declaration.
|
|
14
|
+
*/
|
|
15
|
+
export function signalCallee(
|
|
16
|
+
declaration: ts.VariableDeclaration,
|
|
17
|
+
bindings: ReactiveImportBindings,
|
|
18
|
+
): ReactivePrimitive | undefined {
|
|
19
|
+
const initializer = declaration.initializer
|
|
20
|
+
if (initializer === undefined || !ts.isCallExpression(initializer)) {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
return resolveReactiveExport(initializer.expression, bindings)
|
|
24
|
+
}
|
|
@@ -2,14 +2,16 @@ import ts from 'typescript'
|
|
|
2
2
|
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
Removes effect calls from a script for the SSR back-end.
|
|
6
|
-
lifecycle — they touch the DOM / run side effects and emit no HTML, so the server
|
|
5
|
+
Removes effect/watch reaction calls from a script for the SSR back-end. Reactions are
|
|
6
|
+
client lifecycle — they touch the DOM / run side effects and emit no HTML, so the server
|
|
7
7
|
render (a snapshot of the pre-effect markup, like every framework) must not run
|
|
8
|
-
them.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
becomes a no-op
|
|
12
|
-
|
|
8
|
+
them. Stripped: the bare `effect(<args>)` and its authored scope form
|
|
9
|
+
`scope().effect(<args>)` / `c.effect(<args>)`, and the bare reaction `watch(<args>)`
|
|
10
|
+
(its replacement) — each replaced by `undefined`, so an `effect(() => …)` /
|
|
11
|
+
`watch(src, …)` statement becomes a no-op and a `const stop = watch(…)` binding keeps a
|
|
12
|
+
defined (unused) name. `watch` matches bare only (never a member — unlike the legacy
|
|
13
|
+
`.effect` scope method — so an unrelated `x.watch(...)` is untouched). Client
|
|
14
|
+
compilation keeps reactions untouched.
|
|
13
15
|
*/
|
|
14
16
|
export function stripEffects(code: string): string {
|
|
15
17
|
const source = ts.createSourceFile('script.ts', code, ts.ScriptTarget.Latest, true)
|
|
@@ -34,12 +36,13 @@ export function stripEffectsTransformer(): ts.TransformerFactory<ts.SourceFile>
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
/*
|
|
38
|
-
(`scope().effect`, `c.effect`)
|
|
39
|
-
member name
|
|
39
|
+
/* A reaction callee to strip: the bare runtime helpers (`effect` / `watch`) or the
|
|
40
|
+
scope-method form (`scope().effect`, `c.effect`). Receiver-agnostic on the `.effect`
|
|
41
|
+
member name (effect was a scope primitive); `watch` matches bare only, so an unrelated
|
|
42
|
+
`x.watch(...)` member call is left intact. */
|
|
40
43
|
function isEffectCallee(expression: ts.Expression): boolean {
|
|
41
44
|
if (ts.isIdentifier(expression)) {
|
|
42
|
-
return expression.text === 'effect'
|
|
45
|
+
return expression.text === 'effect' || expression.text === 'watch'
|
|
43
46
|
}
|
|
44
47
|
return ts.isPropertyAccessExpression(expression) && expression.name.text === 'effect'
|
|
45
48
|
}
|
|
@@ -16,6 +16,11 @@ export type AnalyzedComponent = {
|
|
|
16
16
|
/* Top-level import statements hoisted out of the script (e.g. child
|
|
17
17
|
components), placed at module scope by the module wrapper. */
|
|
18
18
|
imports: string
|
|
19
|
+
/* The local binding names of reactive-surface imports (`state`/`effect`) the dead-import
|
|
20
|
+
filter dropped as unused. The module wrapper's backstop asserts none of them still
|
|
21
|
+
appears in the generated output — an independent check that the drop didn't strand a
|
|
22
|
+
live reference (e.g. one a nested branch script keeps literal). */
|
|
23
|
+
droppedReactiveImports: Set<string>
|
|
19
24
|
stateNames: Set<string>
|
|
20
25
|
derivedNames: Set<string>
|
|
21
26
|
computedNames: Set<string>
|