@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CacheSnapshotEntry } from './CacheSnapshotEntry.ts'
|
|
2
|
+
import type { SsrBootState } from './SsrBootState.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The `window.__SSR__` payload: the single contract between the server's SSR state tag
|
|
6
|
+
(`createUiPageRenderer.stateTag`) and the client entry (`startClient`). Both sides
|
|
7
|
+
import THIS type — the server stamps its object with `satisfies SsrPayload` (an unknown
|
|
8
|
+
or mis-typed field is a compile error) and the client reads through it — so the
|
|
9
|
+
write-set and read-set can't drift apart on a matching-string-key handshake.
|
|
10
|
+
|
|
11
|
+
Three partitions:
|
|
12
|
+
- `SsrBootState` fields seed ambient slots via the exhaustive `seedBootState` map;
|
|
13
|
+
- `cache` is the inline warm-seed partition, drained through `seedResolved`;
|
|
14
|
+
- `route`/`params` are stamped for inspectability and are NOT read by the client
|
|
15
|
+
(the router re-resolves the route from the URL).
|
|
16
|
+
*/
|
|
17
|
+
export type SsrPayload = SsrBootState & {
|
|
18
|
+
route: string
|
|
19
|
+
params: Record<string, string>
|
|
20
|
+
cache?: CacheSnapshotEntry[]
|
|
21
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import type { TailHooks } from './TailHooks.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
stream
|
|
8
|
-
|
|
4
|
+
A named async stream: an AsyncIterable carrying a stable `name` used as the
|
|
5
|
+
subscription registry key. Both `Socket<T>` (the declared broadcast
|
|
6
|
+
primitive) and a streaming rpc's bare call (a `jsonl`/`sse` handler makes
|
|
7
|
+
the call return the stream itself) satisfy this shape, so the consume path
|
|
8
|
+
(`watch` → `cache.on`) and the stream probes (`pending`/`refreshing`/
|
|
9
|
+
`done`/`error`) handle either source through one contract.
|
|
9
10
|
|
|
10
11
|
The name on a Socket comes from the file path under `src/server/sockets/`.
|
|
11
|
-
The name on
|
|
12
|
-
|
|
13
|
-
args dedupe to one underlying fetch.
|
|
12
|
+
The name on a streaming rpc call is `keyForRemoteCall(method, url, args)` —
|
|
13
|
+
the same key the smart call's cache store uses — so two readers of the same
|
|
14
|
+
remote-call args dedupe to one underlying fetch.
|
|
14
15
|
|
|
15
16
|
`tail` is the optional retention capability: a source that keeps a tail of
|
|
16
17
|
recent frames hands back an iterable seeded with at most the last `count`
|
|
17
|
-
before going live. Sockets implement it verbatim
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readers can commit atomically; see TailHooks.
|
|
18
|
+
before going live. Sockets implement it verbatim (it also backs the socket's
|
|
19
|
+
HTTP/SSE read face); one-shot rpc streams omit it. Implementers must signal
|
|
20
|
+
`hooks.replayed` in-band once the seed portion is delivered (even when
|
|
21
|
+
empty) so a seeded reader can commit its window atomically; see TailHooks.
|
|
22
22
|
*/
|
|
23
23
|
export interface Subscribable<T> extends AsyncIterable<T> {
|
|
24
24
|
readonly name: string
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Hooks a tail()
|
|
2
|
+
Hooks a `tail(count, hooks)` caller passes to a Subscribable's retention
|
|
3
|
+
capability.
|
|
3
4
|
`replayed` must be signalled in-band exactly once per iteration — after the
|
|
4
5
|
last replayed frame, before any live frame, and even when nothing was
|
|
5
6
|
replayed — so a window reader can commit its seed atomically instead of
|
package/src/lib/shared/url.ts
CHANGED
|
@@ -50,12 +50,17 @@ no bracket segments yields {}, which collapses the params slot away. The
|
|
|
50
50
|
catch-all branch recurses on its head too, so `[name]` segments before a
|
|
51
51
|
`[...rest]` are kept.
|
|
52
52
|
*/
|
|
53
|
-
export type PathParams<P extends string> = P extends `${infer Head}[
|
|
54
|
-
?
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
export type PathParams<P extends string> = P extends `${infer Head}[[${infer Name}]]${infer Tail}`
|
|
54
|
+
? Name extends `...${infer Rest}`
|
|
55
|
+
? // `[[...rest]]` normalizes to a plain catch-all (see parseRouteSegments)
|
|
56
|
+
PathParams<Head> & { [K in Rest]: string | number } & PathParams<Tail>
|
|
57
|
+
: PathParams<Head> & { [K in Name]?: string | number } & PathParams<Tail>
|
|
58
|
+
: P extends `${infer Head}[...${infer Rest}]${infer Tail}`
|
|
59
|
+
? PathParams<Head> & { [K in Rest]: string | number } & PathParams<Tail>
|
|
60
|
+
: P extends `${string}[${infer Name}]${infer Tail}`
|
|
61
|
+
? { [K in Name]: string | number } & PathParams<Tail>
|
|
62
|
+
: // biome-ignore lint/complexity/noBannedTypes: {} is the "no params" base case — keyof {} is never, which collapses the params slot; Record<string, never> would not (keyof is string)
|
|
63
|
+
{}
|
|
59
64
|
|
|
60
65
|
/*
|
|
61
66
|
Resolves any in-app URL to its base-correct, typed form — the single chokepoint
|
|
@@ -93,22 +98,32 @@ export function url(path: string, first?: Query, second?: Query): string {
|
|
|
93
98
|
return appendQuery(prefixed, query)
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
/* Substitutes `[name]` / `[...rest]` segments with their stringified,
|
|
97
|
-
param values (matching the query side, which encodes too). A `[name]`
|
|
98
|
-
encoded whole so `/`, `?`, `#`, and spaces can't alter the path structure;
|
|
99
|
-
`[...rest]` catch-all keeps its `/` separators and encodes each sub-segment.
|
|
101
|
+
/* Substitutes `[name]` / `[[name]]` / `[...rest]` segments with their stringified,
|
|
102
|
+
URL-encoded param values (matching the query side, which encodes too). A `[name]`
|
|
103
|
+
value is encoded whole so `/`, `?`, `#`, and spaces can't alter the path structure;
|
|
104
|
+
a `[...rest]` catch-all keeps its `/` separators and encodes each sub-segment. An
|
|
105
|
+
absent `[[optional]]` — undefined OR empty string, since the matcher never captures
|
|
106
|
+
an empty optional segment — drops its segment (and slash) entirely; a path that
|
|
107
|
+
empties out (`/[[lang]]` with no lang) resolves to the root. Dropping empty here
|
|
108
|
+
also avoids emitting a `//about` that a browser reads as a protocol-relative URL. */
|
|
100
109
|
function interpolate(segments: RouteSegment[], params: Query): string {
|
|
101
|
-
|
|
110
|
+
const resolved = segments
|
|
102
111
|
.map((segment) => {
|
|
103
112
|
if (segment.kind === 'literal') {
|
|
104
113
|
return segment.value
|
|
105
114
|
}
|
|
106
|
-
const value =
|
|
115
|
+
const value = params[segment.name]
|
|
116
|
+
if (segment.optional && (value === undefined || value === '')) {
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
const text = String(value)
|
|
107
120
|
return segment.catchAll
|
|
108
|
-
?
|
|
109
|
-
: encodeURIComponent(
|
|
121
|
+
? text.split('/').map(encodeURIComponent).join('/')
|
|
122
|
+
: encodeURIComponent(text)
|
|
110
123
|
})
|
|
124
|
+
.filter((part) => part !== undefined)
|
|
111
125
|
.join('/')
|
|
126
|
+
return resolved === '' ? '/' : resolved
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
/* Appends a `?`-query built from the same encoder buildRpcRequest uses, or nothing. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cacheStores } from './cacheStores.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Fired once when the outermost hydration pass ends (see the router's hydrate branch
|
|
5
|
+
and `hydrate`). During the pass `peek` withheld every snapshot-seeded warm value
|
|
6
|
+
for SSR congruence (see hydratingSlot); now that the pass is over those values are
|
|
7
|
+
congruent to show, so re-run the scopes that read them. A peek scope taps the exact
|
|
8
|
+
call key's lifecycle channel (peek's trackLifecycle), so marking every live entry's
|
|
9
|
+
key wakes them — cheap, one-time, and harmless for non-peek lifecycle readers (they
|
|
10
|
+
recompute the same value). Marks by key, not the bare store-wide channel, because a
|
|
11
|
+
peek(fn, args) scope taps the per-key prefix channel, which a keyless mark misses.
|
|
12
|
+
*/
|
|
13
|
+
export function wakeHydrationPeeks(): void {
|
|
14
|
+
const stores = cacheStores()
|
|
15
|
+
for (const store of stores) {
|
|
16
|
+
for (const key of store.entries.keys()) {
|
|
17
|
+
store.markLifecycle(key)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -8,7 +8,7 @@ Emits a `.d.ts` that augments createTestApp's `SocketClient` interface with one
|
|
|
8
8
|
entry per $socket, keyed by socket name (the same key `app.sockets.<name>`
|
|
9
9
|
resolves at runtime). The `socket(...)` helper statically returns `Socket<T>`,
|
|
10
10
|
so each entry is the export's own type directly — `app.sockets.chat` types as
|
|
11
|
-
`Socket<ChatMessage>`, iterable for the live stream with `.tail`/`.
|
|
11
|
+
`Socket<ChatMessage>`, iterable for the live stream with `.tail`/`.broadcast`.
|
|
12
12
|
Written to `src/.abide/testSockets.d.ts` like its rpc sibling.
|
|
13
13
|
*/
|
|
14
14
|
export async function writeTestSocketsDts({
|
|
@@ -31,18 +31,18 @@ import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
|
|
|
31
31
|
import { requestContext } from '../server/runtime/requestContext.ts'
|
|
32
32
|
import { resolvePageSnapshot } from '../server/runtime/resolvePageSnapshot.ts'
|
|
33
33
|
import { serverSlot } from '../server/runtime/serverSlot.ts'
|
|
34
|
-
import type { Socket } from '../server/sockets/types/Socket.ts'
|
|
35
34
|
import { baseSlot } from '../shared/baseSlot.ts'
|
|
36
35
|
import { buildRpcProxy } from '../shared/buildRpcProxy.ts'
|
|
37
36
|
import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
|
|
38
37
|
import { cacheStoreSlot } from '../shared/cacheStoreSlot.ts'
|
|
39
38
|
import { commandNameForUrl } from '../shared/commandNameForUrl.ts'
|
|
40
39
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
41
|
-
import { globalCacheStoreSlot } from '../shared/globalCacheStoreSlot.ts'
|
|
42
40
|
import { HEALTH_PATH } from '../shared/HEALTH_PATH.ts'
|
|
43
41
|
import { pageSlot } from '../shared/pageSlot.ts'
|
|
44
42
|
import { SOCKETS_PATH } from '../shared/SOCKETS_PATH.ts'
|
|
43
|
+
import { sharedCacheStoreSlot } from '../shared/sharedCacheStoreSlot.ts'
|
|
45
44
|
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
45
|
+
import type { Socket } from '../shared/types/Socket.ts'
|
|
46
46
|
import { createTestSocketChannel } from './createTestSocketChannel.ts'
|
|
47
47
|
|
|
48
48
|
/*
|
|
@@ -72,7 +72,7 @@ export type TestApp = {
|
|
|
72
72
|
/* Rpc calls over HTTP, keyed by command name: `app.rpc.getProduct({ id })`. */
|
|
73
73
|
rpc: RpcClient
|
|
74
74
|
/* Sockets keyed by name: `app.sockets.ticker` is the Socket — iterate it for
|
|
75
|
-
the live stream, `.tail(n)` to seed, `.
|
|
75
|
+
the live stream, `.tail(n)` to seed, `.broadcast(m)` to send. */
|
|
76
76
|
sockets: SocketClient
|
|
77
77
|
/* The /__abide/health payload, decoded. */
|
|
78
78
|
health: () => Promise<unknown>
|
|
@@ -98,15 +98,15 @@ leaking the request-scope/cache/page resolvers into the next test file.
|
|
|
98
98
|
export async function createTestApp(): Promise<TestApp> {
|
|
99
99
|
const previous = {
|
|
100
100
|
cacheResolver: cacheStoreSlot.resolver,
|
|
101
|
-
|
|
101
|
+
sharedResolver: sharedCacheStoreSlot.resolver,
|
|
102
102
|
pageResolver: pageSlot.resolver,
|
|
103
103
|
baseResolver: baseSlot.resolver,
|
|
104
104
|
activeServer: serverSlot.active,
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const sharedStore = createCacheStore()
|
|
108
|
+
sharedCacheStoreSlot.resolver = () => sharedStore
|
|
109
|
+
cacheStoreSlot.resolver = () => requestContext.getStore()?.cache ?? sharedStore
|
|
110
110
|
pageSlot.resolver = resolvePageSnapshot
|
|
111
111
|
|
|
112
112
|
/* Eager env validation, exactly as serverEntry: a top-level env(schema) in
|
|
@@ -185,7 +185,7 @@ export async function createTestApp(): Promise<TestApp> {
|
|
|
185
185
|
channel?.close()
|
|
186
186
|
server.stop(true)
|
|
187
187
|
cacheStoreSlot.resolver = previous.cacheResolver
|
|
188
|
-
|
|
188
|
+
sharedCacheStoreSlot.resolver = previous.sharedResolver
|
|
189
189
|
pageSlot.resolver = previous.pageResolver
|
|
190
190
|
baseSlot.resolver = previous.baseResolver
|
|
191
191
|
serverSlot.active = previous.activeServer
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Socket } from '../server/sockets/types/Socket.ts'
|
|
2
|
-
import type { SocketClientFrame } from '../server/sockets/types/SocketClientFrame.ts'
|
|
3
|
-
import type { SocketServerFrame } from '../server/sockets/types/SocketServerFrame.ts'
|
|
4
1
|
import { buildSocketOverChannel } from '../shared/buildSocketOverChannel.ts'
|
|
5
2
|
import { createSocketSubRegistry } from '../shared/createSocketSubRegistry.ts'
|
|
6
3
|
import { decodeRefJson } from '../shared/decodeRefJson.ts'
|
|
7
4
|
import { encodeRefJson } from '../shared/encodeRefJson.ts'
|
|
5
|
+
import type { Socket } from '../shared/types/Socket.ts'
|
|
6
|
+
import type { SocketClientFrame } from '../shared/types/SocketClientFrame.ts'
|
|
7
|
+
import type { SocketServerFrame } from '../shared/types/SocketServerFrame.ts'
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
10
|
Test-side substitute for the browser socketChannel: one ws to the booted
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
/* The
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
the
|
|
5
|
-
|
|
6
|
-
only the membership set, so a new primitive is a single edit here. */
|
|
1
|
+
/* The author-facing reactive primitive names (`state`, `state.linked`, `state.computed`,
|
|
2
|
+
and the destructuring `const {…} = props()`). The compiler no longer reads this —
|
|
3
|
+
recognition is import-resolved (see `resolveReactiveExport.ts`). It remains the canonical
|
|
4
|
+
inventory the docs surface-weighting tool enumerates (`scripts/surfaceWeight.ts`), so the
|
|
5
|
+
grammar's "primitives" bucket stays in sync with the language without hardcoding a list. */
|
|
7
6
|
export const REACTIVE_CALLEES: ReadonlySet<string> = new Set([
|
|
8
7
|
'state',
|
|
9
8
|
'linked',
|
|
@@ -13,13 +13,16 @@ flip to its `$$` alias, set `alias` here in lockstep.
|
|
|
13
13
|
*/
|
|
14
14
|
export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: string }[] = [
|
|
15
15
|
{ name: 'snippet', specifier: 'shared/snippet', alias: '$$snippet' },
|
|
16
|
-
/* `scope` is author-facing
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{ name: '
|
|
16
|
+
/* `scope` is the internal lowering host (no longer author-facing), imported from the
|
|
17
|
+
plumbing `ui/currentScope` path and lowered to the reserved `$$scope` alias:
|
|
18
|
+
`referenceFor` rewrites a bare `scope` read (compiler-generated) to `$$scope` only
|
|
19
|
+
when not lexically shadowed, so the aliased import is the binding emitted code
|
|
20
|
+
resolves to. `enterScope`/`exitScope` bracket an SSR render's scope (plumbing paths). */
|
|
21
|
+
{ name: 'scope', specifier: 'ui/currentScope', alias: '$$scope' },
|
|
22
|
+
{ name: 'enterScope', specifier: 'ui/enterRenderScope', alias: '$$enterScope' },
|
|
23
|
+
{ name: 'exitScope', specifier: 'ui/exitRenderScope', alias: '$$exitScope' },
|
|
22
24
|
{ name: 'effect', specifier: 'ui/effect', alias: '$$effect' },
|
|
25
|
+
{ name: 'watch', specifier: 'ui/watch', alias: '$$watch' },
|
|
23
26
|
{ name: 'mount', specifier: 'ui/dom/mount', alias: '$$mount' },
|
|
24
27
|
{ name: 'appendText', specifier: 'ui/dom/appendText', alias: '$$appendText' },
|
|
25
28
|
{ name: 'appendTextAt', specifier: 'ui/dom/appendTextAt', alias: '$$appendTextAt' },
|
|
@@ -31,11 +34,11 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: stri
|
|
|
31
34
|
{ name: 'attr', specifier: 'ui/dom/attr', alias: '$$attr' },
|
|
32
35
|
{ name: 'on', specifier: 'ui/dom/on', alias: '$$on' },
|
|
33
36
|
{ name: 'attach', specifier: 'ui/dom/attach', alias: '$$attach' },
|
|
37
|
+
{ name: 'bindSelectValue', specifier: 'ui/dom/bindSelectValue', alias: '$$bindSelectValue' },
|
|
34
38
|
{ name: 'each', specifier: 'ui/dom/each', alias: '$$each' },
|
|
35
39
|
{ name: 'eachAsync', specifier: 'ui/dom/eachAsync', alias: '$$eachAsync' },
|
|
36
40
|
{ name: 'when', specifier: 'ui/dom/when', alias: '$$when' },
|
|
37
41
|
{ name: 'awaitBlock', specifier: 'ui/dom/awaitBlock', alias: '$$awaitBlock' },
|
|
38
|
-
{ name: 'deferResume', specifier: 'ui/deferResume', alias: '$$deferResume' },
|
|
39
42
|
{ name: 'tryBlock', specifier: 'ui/dom/tryBlock', alias: '$$tryBlock' },
|
|
40
43
|
{ name: 'switchBlock', specifier: 'ui/dom/switchBlock', alias: '$$switchBlock' },
|
|
41
44
|
{ name: 'mountSlot', specifier: 'ui/dom/mountSlot', alias: '$$mountSlot' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
2
|
import type { BunPlugin } from 'bun'
|
|
3
|
-
import {
|
|
3
|
+
import { isLayoutFile } from '../../shared/isLayoutFile.ts'
|
|
4
4
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
5
5
|
import { AbideCompileError } from './AbideCompileError.ts'
|
|
6
6
|
import { compileModule } from './compileModule.ts'
|
|
@@ -42,7 +42,7 @@ export const abideUiPlugin: BunPlugin = {
|
|
|
42
42
|
build.onLoad({ filter: /\.abide$/ }, async (args) => {
|
|
43
43
|
const source = await Bun.file(args.path).text()
|
|
44
44
|
const moduleId = relative(nearestProjectRoot(args.path, process.cwd()), args.path)
|
|
45
|
-
const isLayout =
|
|
45
|
+
const isLayout = isLayoutFile(args.path)
|
|
46
46
|
/* Bun frames a plugin throw at `<file>:0` regardless of the real spot, so
|
|
47
47
|
carry the component path + resolved line:col in the message — otherwise a
|
|
48
48
|
control-flow / compile error reads as `:0` and (in deep imports) can look
|
|
@@ -29,6 +29,12 @@ export function analyzeComponent(source: string, scopeSeed?: string): AnalyzedCo
|
|
|
29
29
|
const scriptBody = (scriptMatch?.[1] ?? '').trim()
|
|
30
30
|
const template = source.replace(/^\s*<script[^>]*>[\s\S]*?<\/script>/, '').trim()
|
|
31
31
|
|
|
32
|
+
/* Parse the template first so the nested branch `<script>`s are in hand before the
|
|
33
|
+
leading script lowers: their raw code feeds the dead-import usage check, keeping a
|
|
34
|
+
reactive import (`state`) alive when only a nested branch uses it (its
|
|
35
|
+
`state.computed(...)` stays literal, unlike the desugared leading script). */
|
|
36
|
+
const { nodes } = parseTemplate(template)
|
|
37
|
+
const nestedScriptCode = collectNestedScriptCode(nodes)
|
|
32
38
|
/* `lowerScript` parses the script ONCE and chains signal desugaring, reference
|
|
33
39
|
renaming, and doc-access lowering over that single tree, then hoists top-level
|
|
34
40
|
imports off the tree structurally — imports live at module scope, not inside the
|
|
@@ -40,17 +46,16 @@ export function analyzeComponent(source: string, scopeSeed?: string): AnalyzedCo
|
|
|
40
46
|
stateNames,
|
|
41
47
|
derivedNames,
|
|
42
48
|
computedNames,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
scoped CSS per block for the bundler. */
|
|
48
|
-
const { nodes } = parseTemplate(template)
|
|
49
|
+
droppedReactiveImports,
|
|
50
|
+
} = lowerScript(scriptBody, nestedScriptCode)
|
|
51
|
+
/* `annotateScopes` mutates the tree — assigning each style its scope attribute and
|
|
52
|
+
stamping covered elements — and returns the scoped CSS per block for the bundler. */
|
|
49
53
|
const styles = annotateScopes(nodes, [], scopeSeed, { count: 0 })
|
|
50
54
|
return {
|
|
51
55
|
script,
|
|
52
56
|
ssrScript,
|
|
53
57
|
imports,
|
|
58
|
+
droppedReactiveImports,
|
|
54
59
|
stateNames,
|
|
55
60
|
derivedNames,
|
|
56
61
|
computedNames,
|
|
@@ -68,6 +73,23 @@ function childrenOf(node: TemplateNode): TemplateNode[] | undefined {
|
|
|
68
73
|
return 'children' in node ? node.children : undefined
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
/* The concatenated source of every nested `<script>` anywhere in the tree — the branch-local
|
|
77
|
+
reactive blocks. Used only to keep the leading script's reactive imports alive when a nested
|
|
78
|
+
branch is their sole consumer; the raw text is enough to spot the identifier reference. */
|
|
79
|
+
function collectNestedScriptCode(nodes: TemplateNode[]): string {
|
|
80
|
+
let code = ''
|
|
81
|
+
for (const node of nodes) {
|
|
82
|
+
if (node.kind === 'script') {
|
|
83
|
+
code += `\n${node.code}`
|
|
84
|
+
}
|
|
85
|
+
const children = childrenOf(node)
|
|
86
|
+
if (children !== undefined) {
|
|
87
|
+
code += collectNestedScriptCode(children)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return code
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
/*
|
|
72
94
|
Walks `nodes` in source order, scoping each `<style>` to its sibling subtree: a
|
|
73
95
|
style among a sibling list covers every element in that list AND every descendant
|
|
@@ -121,6 +121,24 @@ ${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(
|
|
|
121
121
|
/* setParentNodes */ true,
|
|
122
122
|
)
|
|
123
123
|
const referenced = collectIdentifiers(bodySource)
|
|
124
|
+
/* Independent backstop on the reactive-import drop (`deadReactiveImport`): it decides an
|
|
125
|
+
import is dead from the leading script's own lowered body plus the nested scripts' raw
|
|
126
|
+
text. This re-checks the SAME question against the FINAL generated output — so a drop that
|
|
127
|
+
stranded a live reference (e.g. a nested branch's literal `state.computed`) surfaces as a
|
|
128
|
+
located compile error, not a runtime `ReferenceError: state is not defined`. Mirrors
|
|
129
|
+
`assertRuntimeHelpersBound`, which guards the parallel runtime-helper drop. Uses VALUE
|
|
130
|
+
references (not every identifier): the SSR return object `{ html, state, awaits, resume }`
|
|
131
|
+
names a property `state`, which is not a use of the reactive binding. */
|
|
132
|
+
if (analyzed.droppedReactiveImports.size > 0) {
|
|
133
|
+
const valueReferences = collectValueReferences(bodySource)
|
|
134
|
+
for (const name of analyzed.droppedReactiveImports) {
|
|
135
|
+
if (valueReferences.has(name)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[abide] component module generation dropped the reactive import \`${name}\` as unused, but the generated output still references it — the dead-import filter undercounted. Please report this with the component source.`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
124
142
|
const keptImports = UI_RUNTIME_IMPORTS.filter((entry) =>
|
|
125
143
|
referenced.has(entry.alias ?? entry.name),
|
|
126
144
|
)
|
|
@@ -168,24 +186,90 @@ function indent(body: string): string {
|
|
|
168
186
|
.split('\n')
|
|
169
187
|
.map((line) => {
|
|
170
188
|
const indented = insideTemplateLiteral || line === '' ? line : ` ${line}`
|
|
171
|
-
|
|
172
|
-
insideTemplateLiteral = !insideTemplateLiteral
|
|
173
|
-
}
|
|
189
|
+
insideTemplateLiteral = templateLiteralStateAfter(line, insideTemplateLiteral)
|
|
174
190
|
return indented
|
|
175
191
|
})
|
|
176
192
|
.join('\n')
|
|
177
193
|
}
|
|
178
194
|
|
|
179
|
-
/*
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
/* Whether the line ENDS inside a multi-line template literal, given whether it
|
|
196
|
+
STARTED inside one. A plain backtick-parity count is wrong: a backtick inside a
|
|
197
|
+
double-quoted string literal (the JSON.stringify'd skeleton markup, e.g.
|
|
198
|
+
`$$skeleton(host, "<p>tick \`here</p>")`) is not a template delimiter, but counting
|
|
199
|
+
it would flip the in-literal state and mis-indent a later real template literal's
|
|
200
|
+
significant whitespace. So skip `'…'`/`"…"` strings when outside a template, and treat
|
|
201
|
+
a template's `${…}` interpolation as code (its own strings/backticks don't toggle the
|
|
202
|
+
outer literal). Escapes are honoured throughout. */
|
|
203
|
+
function templateLiteralStateAfter(line: string, startInside: boolean): boolean {
|
|
204
|
+
let inside = startInside
|
|
205
|
+
let index = 0
|
|
206
|
+
while (index < line.length) {
|
|
207
|
+
const char = line[index]
|
|
208
|
+
if (char === '\\') {
|
|
209
|
+
index += 2
|
|
210
|
+
continue
|
|
186
211
|
}
|
|
212
|
+
if (inside) {
|
|
213
|
+
if (char === '`') {
|
|
214
|
+
inside = false
|
|
215
|
+
} else if (char === '$' && line[index + 1] === '{') {
|
|
216
|
+
index = skipInterpolation(line, index + 2)
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
index += 1
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (char === '`') {
|
|
223
|
+
inside = true
|
|
224
|
+
} else if (char === '"' || char === "'") {
|
|
225
|
+
index = skipQuoted(line, index + 1, char)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
index += 1
|
|
187
229
|
}
|
|
188
|
-
return
|
|
230
|
+
return inside
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Advances past a `'…'`/`"…"` string opened at `index` (the char after the quote),
|
|
234
|
+
returning the index after the closing quote (or end of line). Emitted JS string
|
|
235
|
+
literals never carry a raw newline, so they always close on their own line. */
|
|
236
|
+
function skipQuoted(line: string, index: number, quote: string): number {
|
|
237
|
+
while (index < line.length) {
|
|
238
|
+
if (line[index] === '\\') {
|
|
239
|
+
index += 2
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (line[index] === quote) {
|
|
243
|
+
return index + 1
|
|
244
|
+
}
|
|
245
|
+
index += 1
|
|
246
|
+
}
|
|
247
|
+
return index
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Advances past a template `${…}` interpolation opened at `index` (the char after `{`),
|
|
251
|
+
returning the index after the matching `}` (or end of line). Balances nested braces and
|
|
252
|
+
skips nested strings/templates so their delimiters don't leak into the outer scan. */
|
|
253
|
+
function skipInterpolation(line: string, index: number): number {
|
|
254
|
+
let depth = 1
|
|
255
|
+
while (index < line.length && depth > 0) {
|
|
256
|
+
const char = line[index]
|
|
257
|
+
if (char === '\\') {
|
|
258
|
+
index += 2
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
262
|
+
index = skipQuoted(line, index + 1, char)
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (char === '{') {
|
|
266
|
+
depth += 1
|
|
267
|
+
} else if (char === '}') {
|
|
268
|
+
depth -= 1
|
|
269
|
+
}
|
|
270
|
+
index += 1
|
|
271
|
+
}
|
|
272
|
+
return index
|
|
189
273
|
}
|
|
190
274
|
|
|
191
275
|
/* The identifier names a generated module references — every Identifier node, walked
|
|
@@ -209,3 +293,45 @@ function collectIdentifiers(source: ts.SourceFile): Set<string> {
|
|
|
209
293
|
visit(source)
|
|
210
294
|
return names
|
|
211
295
|
}
|
|
296
|
+
|
|
297
|
+
/* The identifiers a module references AS VALUES — the subset of `collectIdentifiers` that
|
|
298
|
+
excludes name-only positions: a property name (`obj.state`), an object-literal key
|
|
299
|
+
(`{ state: v }`), and a declaration/import binding name (the definition, not a use). Used by
|
|
300
|
+
the reactive-import backstop, where a bare identifier presence over-counts: the synthesized
|
|
301
|
+
SSR return `{ html, state, awaits, resume }` names a property `state` that is not a use of the
|
|
302
|
+
reactive binding, so the raw-identifier set would false-positive. */
|
|
303
|
+
function collectValueReferences(source: ts.SourceFile): Set<string> {
|
|
304
|
+
const names = new Set<string>()
|
|
305
|
+
const visit = (node: ts.Node): void => {
|
|
306
|
+
if (ts.isIdentifier(node) && isValuePosition(node)) {
|
|
307
|
+
names.add(node.text)
|
|
308
|
+
}
|
|
309
|
+
node.forEachChild(visit)
|
|
310
|
+
}
|
|
311
|
+
visit(source)
|
|
312
|
+
return names
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* True when `id` stands in a value position — i.e. a reference to a binding, not a name that
|
|
316
|
+
merely labels a property or declares a binding. */
|
|
317
|
+
function isValuePosition(id: ts.Identifier): boolean {
|
|
318
|
+
const parent = id.parent
|
|
319
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === id) {
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
if (ts.isPropertyAssignment(parent) && parent.name === id) {
|
|
323
|
+
return false
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
(ts.isVariableDeclaration(parent) ||
|
|
327
|
+
ts.isParameter(parent) ||
|
|
328
|
+
ts.isFunctionDeclaration(parent) ||
|
|
329
|
+
ts.isBindingElement(parent) ||
|
|
330
|
+
ts.isImportSpecifier(parent) ||
|
|
331
|
+
ts.isImportClause(parent)) &&
|
|
332
|
+
parent.name === id
|
|
333
|
+
) {
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
return true
|
|
337
|
+
}
|