@abide/abide 0.33.1 → 0.34.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 +39 -34
- package/CHANGELOG.md +24 -0
- package/README.md +21 -21
- package/package.json +5 -5
- package/src/abideResolverPlugin.ts +1 -1
- package/src/build.ts +1 -1
- package/src/compile.ts +1 -1
- package/src/lib/bundle/BundleMenu.ts +1 -1
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/bundle/BundleWindow.ts +1 -1
- package/src/lib/bundle/bundled.ts +1 -1
- package/src/lib/bundle/disconnected.abide +105 -75
- package/src/lib/bundle/onMenu.ts +1 -1
- package/src/lib/mcp/createMcpServer.ts +1 -1
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/DELETE.ts +1 -1
- package/src/lib/server/GET.ts +1 -1
- package/src/lib/server/HEAD.ts +1 -1
- package/src/lib/server/PATCH.ts +1 -1
- package/src/lib/server/POST.ts +1 -1
- package/src/lib/server/PUT.ts +1 -1
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/appDataDir.ts +1 -1
- package/src/lib/server/cookies.ts +1 -1
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +1 -1
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/prompts/definePrompt.ts +1 -1
- package/src/lib/server/prompts/renderPromptTemplate.ts +1 -1
- package/src/lib/server/reachable.ts +1 -1
- package/src/lib/server/redirect.ts +1 -1
- package/src/lib/server/request.ts +1 -1
- package/src/lib/server/rpc/defineVerb.ts +1 -1
- package/src/lib/server/runtime/buildCacheSnapshot.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +2 -2
- package/src/lib/server/runtime/types/InspectorContext.ts +2 -2
- package/src/lib/server/server.ts +1 -1
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/defineSocket.ts +1 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/cache.ts +18 -18
- package/src/lib/shared/createSubscriber.ts +1 -1
- package/src/lib/shared/health.ts +1 -1
- package/src/lib/shared/html.ts +1 -1
- package/src/lib/shared/isSubscribable.ts +1 -1
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/page.ts +1 -1
- package/src/lib/shared/pending.ts +2 -2
- package/src/lib/shared/probeRegistries.ts +2 -2
- package/src/lib/shared/refreshing.ts +2 -2
- package/src/lib/shared/selectorMatcher.ts +8 -8
- package/src/lib/shared/selectorPrefix.ts +1 -1
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/toTagSet.ts +4 -0
- package/src/lib/shared/trace.ts +1 -1
- package/src/lib/shared/types/CacheEntry.ts +3 -3
- package/src/lib/shared/types/CacheOptions.ts +3 -3
- package/src/lib/shared/types/CacheSelector.ts +2 -2
- package/src/lib/shared/url.ts +1 -1
- package/src/lib/shared/withJsonSchema.ts +1 -1
- package/src/lib/test/assertAgentFrameConformance.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +1 -1
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -2
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +3 -4
- package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
- package/src/lib/ui/compile/analyzeComponent.ts +2 -1
- package/src/lib/ui/compile/compileComponent.ts +3 -3
- package/src/lib/ui/compile/compileModule.ts +30 -2
- package/src/lib/ui/compile/compileSSR.ts +5 -5
- package/src/lib/ui/compile/compileShadow.ts +54 -36
- package/src/lib/ui/compile/desugarSignals.ts +147 -18
- package/src/lib/ui/compile/generateBuild.ts +15 -28
- package/src/lib/ui/compile/generateSSR.ts +20 -13
- package/src/lib/ui/compile/isControlFlow.ts +18 -0
- package/src/lib/ui/compile/isTextLeaf.ts +15 -0
- package/src/lib/ui/compile/lowerContext.ts +45 -5
- package/src/lib/ui/compile/prepareNestedScript.ts +12 -8
- package/src/lib/ui/compile/renameSignalRefs.ts +21 -4
- package/src/lib/ui/compile/skeletonable.ts +1 -14
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +1 -0
- package/src/lib/ui/computed.ts +32 -0
- package/src/lib/ui/createScope.ts +91 -0
- package/src/lib/ui/dom/anchorCursor.ts +1 -1
- package/src/lib/ui/dom/appendSnippet.ts +1 -1
- package/src/lib/ui/dom/appendStatic.ts +1 -1
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/appendTextAt.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/attach.ts +7 -2
- package/src/lib/ui/dom/attr.ts +1 -1
- package/src/lib/ui/dom/awaitBlock.ts +1 -1
- package/src/lib/ui/dom/cloneStatic.ts +1 -1
- package/src/lib/ui/dom/each.ts +4 -3
- package/src/lib/ui/dom/eachAsync.ts +1 -1
- package/src/lib/ui/dom/hydrate.ts +20 -6
- package/src/lib/ui/dom/mount.ts +12 -1
- package/src/lib/ui/dom/mountChild.ts +6 -2
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/on.ts +10 -4
- package/src/lib/ui/dom/openMarker.ts +3 -3
- package/src/lib/ui/dom/skeleton.ts +13 -5
- package/src/lib/ui/dom/switchBlock.ts +1 -1
- package/src/lib/ui/dom/text.ts +1 -1
- package/src/lib/ui/dom/tryBlock.ts +1 -1
- package/src/lib/ui/dom/when.ts +1 -1
- package/src/lib/ui/effect.ts +7 -2
- package/src/lib/ui/enterScope.ts +18 -0
- package/src/lib/ui/exitScope.ts +8 -0
- package/src/lib/ui/history.ts +102 -0
- package/src/lib/ui/installHotBridge.ts +6 -8
- package/src/lib/ui/linked.ts +0 -1
- package/src/lib/ui/navigate.ts +1 -1
- package/src/lib/ui/outbox.ts +115 -0
- package/src/lib/ui/persist.ts +112 -0
- package/src/lib/ui/probeNavigation.ts +1 -1
- package/src/lib/ui/remoteProxy.ts +1 -1
- package/src/lib/ui/renderToStream.ts +1 -1
- package/src/lib/ui/router.ts +1 -1
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +10 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +1 -1
- package/src/lib/ui/runtime/PATCH_BUS.ts +28 -0
- package/src/lib/ui/runtime/RESUME.ts +1 -1
- package/src/lib/ui/runtime/captureModelDoc.ts +29 -0
- package/src/lib/ui/runtime/claimExpected.ts +26 -0
- package/src/lib/ui/runtime/createComputedNode.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +73 -3
- package/src/lib/ui/runtime/enterRenderPass.ts +1 -1
- package/src/lib/ui/runtime/escapeKey.ts +13 -0
- package/src/lib/ui/runtime/exitRenderPass.ts +1 -1
- package/src/lib/ui/runtime/hotReplace.ts +17 -8
- package/src/lib/ui/runtime/inScope.ts +19 -0
- package/src/lib/ui/runtime/localStoragePersistence.ts +34 -0
- package/src/lib/ui/runtime/nextBlockId.ts +1 -1
- package/src/lib/ui/runtime/seedModelDoc.ts +26 -0
- package/src/lib/ui/runtime/types/{Derived.ts → Computed.ts} +1 -1
- package/src/lib/ui/runtime/types/Doc.ts +3 -0
- package/src/lib/ui/runtime/types/HotInstance.ts +6 -2
- package/src/lib/ui/runtime/types/PatchEvent.ts +16 -0
- package/src/lib/ui/runtime/unescapeKey.ts +12 -0
- package/src/lib/ui/runtime/walkPath.ts +3 -1
- package/src/lib/ui/scope.ts +19 -0
- package/src/lib/ui/socketProxy.ts +1 -1
- package/src/lib/ui/startClient.ts +1 -1
- package/src/lib/ui/state.ts +1 -2
- package/src/lib/ui/sync.ts +42 -0
- package/src/lib/ui/tail.ts +1 -1
- package/src/lib/ui/types/History.ts +14 -0
- package/src/lib/ui/types/Outbox.ts +14 -0
- package/src/lib/ui/types/PersistHandle.ts +11 -0
- package/src/lib/ui/types/PersistenceStore.ts +12 -0
- package/src/lib/ui/types/Scope.ts +57 -0
- package/src/lib/ui/types/SyncTransport.ts +13 -0
- package/src/preload.ts +1 -1
- package/src/lib/shared/toScopeSet.ts +0 -4
- package/src/lib/ui/derived.ts +0 -49
- package/src/lib/ui/doc.ts +0 -15
package/src/lib/shared/health.ts
CHANGED
|
@@ -13,7 +13,7 @@ Augmented by the generated `src/.abide/health.d.ts` with
|
|
|
13
13
|
`health()` reads type against its own hook. Unaugmented (no app.ts, or no
|
|
14
14
|
hook), AppHealth resolves to no fields.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation observability
|
|
17
17
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated health.d.ts
|
|
18
18
|
export interface AppHealthMap {}
|
|
19
19
|
export type AppHealth = AppHealthMap extends { fields: infer Fields }
|
package/src/lib/shared/html.ts
CHANGED
|
@@ -17,7 +17,7 @@ Calling `html` is the explicit opt-in to raw insertion; plain `{value}` always
|
|
|
17
17
|
escapes. The tag does NOT auto-escape interpolations (it's raw by intent), so only
|
|
18
18
|
build markup from values you trust, or escape them yourself.
|
|
19
19
|
*/
|
|
20
|
-
// @
|
|
20
|
+
// @documentation templating
|
|
21
21
|
export function html(strings: TemplateStringsArray | string, ...values: unknown[]): RawHtml {
|
|
22
22
|
if (typeof strings === 'string') {
|
|
23
23
|
return { [RAW_HTML]: strings }
|
|
@@ -2,7 +2,7 @@ import type { Subscribable } from './types/Subscribable.ts'
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
A Subscribable is a named AsyncIterable — distinguishes a stream argument from
|
|
5
|
-
the other probe selector shapes (callables and `{
|
|
5
|
+
the other probe selector shapes (callables and `{ tags }` objects, neither of
|
|
6
6
|
which carries Symbol.asyncIterator).
|
|
7
7
|
*/
|
|
8
8
|
export function isSubscribable(value: unknown): value is Subscribable<unknown> {
|
package/src/lib/shared/log.ts
CHANGED
|
@@ -13,7 +13,7 @@ diagnostic channel (browser: the `abide-debug` localStorage key). Renders as
|
|
|
13
13
|
the tab-separated tsv format (default) or one JSON object per line under
|
|
14
14
|
ABIDE_LOG_FORMAT=json.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation observability
|
|
17
17
|
export const log: Log = Object.assign(
|
|
18
18
|
createChannelLog(() => appNameSlot.name ?? 'app', true),
|
|
19
19
|
{
|
package/src/lib/shared/online.ts
CHANGED
package/src/lib/shared/page.ts
CHANGED
|
@@ -13,7 +13,7 @@ navigation updates the snapshot (the client resolver reads a abide-ui signal).
|
|
|
13
13
|
prefix (the server re-applies it to the proxy-stripped request URL), so compare
|
|
14
14
|
active state against url() output, e.g. `page.url.pathname.startsWith(url('/x'))`.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation page
|
|
17
17
|
export const page: PageSnapshot = {
|
|
18
18
|
get route(): string {
|
|
19
19
|
return activePage().route
|
|
@@ -11,7 +11,7 @@ tail registry (streams). Pending means "no value yet":
|
|
|
11
11
|
pending(fn) → that function's calls (per-route spinner; remote or
|
|
12
12
|
producer, same selector grammar as cache.invalidate)
|
|
13
13
|
pending(fn, args) → exactly that call (per-row spinner)
|
|
14
|
-
pending({
|
|
14
|
+
pending({ tags }) → a tagged group
|
|
15
15
|
pending(subscribable) → that stream awaiting its first frame
|
|
16
16
|
(tail.status === 'pending'; true when nothing
|
|
17
17
|
is reading yet — there is no value either way)
|
|
@@ -19,7 +19,7 @@ Probes report, never act: reading one opens no fetch and no stream. SSR
|
|
|
19
19
|
loading state is driven by {#await}, not this. Scan semantics (tap order,
|
|
20
20
|
selector grammar, registry spans) live in probeRegistries.
|
|
21
21
|
*/
|
|
22
|
-
// @
|
|
22
|
+
// @documentation probes
|
|
23
23
|
export function pending<Args, Return>(
|
|
24
24
|
arg?: CacheSelector<Args, Return> | Subscribable<unknown>,
|
|
25
25
|
args?: Args,
|
|
@@ -16,14 +16,14 @@ re-runs whenever a matching call or stream changes state. The tap is scoped
|
|
|
16
16
|
to the selector: a fn selector taps only its own prefix channel, so reading
|
|
17
17
|
`pending(fn)` in an effect can't be re-woken by unrelated cache events (an
|
|
18
18
|
effect that probes one fn and invalidates another no longer feeds itself);
|
|
19
|
-
bare and
|
|
19
|
+
bare and tag selectors scan many entries and tap the store-wide channel. A
|
|
20
20
|
producer never cached resolves no prefix yet and taps store-wide — its first
|
|
21
21
|
cache() marks that channel, the re-derive resolves the prefix, and the scope
|
|
22
22
|
re-tracks narrowed. `matchesEntry` is the probe's cache-side question;
|
|
23
23
|
`field` selects the stream-side answer; `unprobed` is the answer for a
|
|
24
24
|
stream no prober can see (server, or tail never imported) — matching what
|
|
25
25
|
tail() itself reports there. The bare form (no selector) spans both
|
|
26
|
-
registries; fn/
|
|
26
|
+
registries; fn/tag selectors are cache identities only. Outside a tracking
|
|
27
27
|
scope the taps are no-ops and the current value returns.
|
|
28
28
|
*/
|
|
29
29
|
export function probeRegistries<Args, Return>(
|
|
@@ -9,7 +9,7 @@ value it already had while a fresher source is in flight":
|
|
|
9
9
|
refreshing() → anything reloading data it already had
|
|
10
10
|
refreshing(fn) → that function's calls (per-route "updating…" badge)
|
|
11
11
|
refreshing(fn, args) → exactly that call (per-row badge)
|
|
12
|
-
refreshing({
|
|
12
|
+
refreshing({ tags }) → a tagged group
|
|
13
13
|
refreshing(subscribable) → that stream reconnecting with its last value
|
|
14
14
|
retained — never merely `open`; a live stream's
|
|
15
15
|
arriving frames are its normal mode, not a reload
|
|
@@ -20,7 +20,7 @@ no value yet?", refreshing answers "is a held value being superseded?".
|
|
|
20
20
|
Probes report, never act. Scan semantics (tap order, selector grammar,
|
|
21
21
|
registry spans) live in probeRegistries.
|
|
22
22
|
*/
|
|
23
|
-
// @
|
|
23
|
+
// @documentation probes
|
|
24
24
|
export function refreshing<Args, Return>(
|
|
25
25
|
arg?: CacheSelector<Args, Return> | Subscribable<unknown>,
|
|
26
26
|
args?: Args,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { keyMatchesPrefix } from './keyMatchesPrefix.ts'
|
|
2
2
|
import { selectorPrefix } from './selectorPrefix.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { toTagSet } from './toTagSet.ts'
|
|
4
4
|
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
5
5
|
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ pending(), and refreshing() so all three interpret the call shapes identically:
|
|
|
16
16
|
producer fn → that producer's calls (reference id prefix). Matches
|
|
17
17
|
only if the producer was cached at least once (else it
|
|
18
18
|
has no id and nothing matches).
|
|
19
|
-
{
|
|
19
|
+
{ tags } → any entry sharing one of the requested tags. An
|
|
20
20
|
empty selector matches nothing.
|
|
21
21
|
fn + args → exactly that call's entry (the key derived from the
|
|
22
22
|
same encoders the read path uses); other args
|
|
@@ -46,11 +46,11 @@ export function selectorMatcher<Args, Return>(
|
|
|
46
46
|
}
|
|
47
47
|
return (entry) => keyMatchesPrefix(entry.key, prefix)
|
|
48
48
|
}
|
|
49
|
-
if (arg.
|
|
49
|
+
if (arg.tags === undefined) {
|
|
50
50
|
return () => false
|
|
51
51
|
}
|
|
52
|
-
const
|
|
53
|
-
return (entry) => entry.
|
|
52
|
+
const requestedTags = toTagSet(arg.tags)
|
|
53
|
+
return (entry) => entry.tags !== undefined && intersects(entry.tags, requestedTags)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/*
|
|
@@ -58,9 +58,9 @@ True when an entry's tags and the requested tags overlap on any tag. A plain
|
|
|
58
58
|
for-of over the Set rather than .values().some() — Iterator Helpers are too new
|
|
59
59
|
for this module's browser baseline (see producerKey).
|
|
60
60
|
*/
|
|
61
|
-
function intersects(
|
|
62
|
-
for (const
|
|
63
|
-
if (
|
|
61
|
+
function intersects(entryTags: Set<string>, requestedTags: Set<string>): boolean {
|
|
62
|
+
for (const tag of requestedTags) {
|
|
63
|
+
if (entryTags.has(tag)) {
|
|
64
64
|
return true
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -11,7 +11,7 @@ method+url (fn and fn.raw agree — same wire identity), a producer on the
|
|
|
11
11
|
reference id minted at first cache. With `args`, the exact entry key for that
|
|
12
12
|
one call instead — derived through the same encoders the read path uses
|
|
13
13
|
(keyForRemoteCall / producerKey format), so selector and entry can't
|
|
14
|
-
disagree. Undefined when no prefix exists — bare and
|
|
14
|
+
disagree. Undefined when no prefix exists — bare and tag selectors (they
|
|
15
15
|
scan, not prefix-match) and producers never cached (no id was minted;
|
|
16
16
|
minting one here would leak identities for probe-only reads). The
|
|
17
17
|
cache()-wrapper throw lives here so both consumers — the matcher and the
|
|
@@ -10,7 +10,7 @@ export type Snippet<Payload> = { readonly [SNIPPET]: Payload }
|
|
|
10
10
|
inserting escaped text. The compiler wraps a snippet's body in this — the client
|
|
11
11
|
builder closes over the defining component's scope, the server string is its SSR
|
|
12
12
|
render — so a snippet value passes through props like any other value. */
|
|
13
|
-
// @
|
|
13
|
+
// @documentation templating
|
|
14
14
|
export function snippet<Payload>(payload: Payload): Snippet<Payload> {
|
|
15
15
|
return { [SNIPPET]: payload }
|
|
16
16
|
}
|
package/src/lib/shared/trace.ts
CHANGED
|
@@ -9,7 +9,7 @@ __SSR__ — so a client-side read returns the trace of the request that
|
|
|
9
9
|
rendered the page. The string is propagation-ready: hand it to your own logs,
|
|
10
10
|
error reports, or `propagation.extract` when attaching OpenTelemetry spans.
|
|
11
11
|
*/
|
|
12
|
-
// @
|
|
12
|
+
// @documentation observability
|
|
13
13
|
export function trace(): string | undefined {
|
|
14
14
|
const scope = requestScopeSlot.resolver?.()
|
|
15
15
|
return scope ? formatTraceparent(scope.trace) : undefined
|
|
@@ -28,8 +28,8 @@ ttl = 0 = the warm value exists only to complete the hydration render and is
|
|
|
28
28
|
evicted a macrotask later). Live entries never carry it; their ttl was fixed
|
|
29
29
|
at registration.
|
|
30
30
|
|
|
31
|
-
`
|
|
32
|
-
`cache.invalidate({
|
|
31
|
+
`tags` holds the cache() call's tags as a Set so
|
|
32
|
+
`cache.invalidate({ tags })` can drop every entry sharing any tag with O(1)
|
|
33
33
|
membership; a re-read merges new tags in rather than replacing them.
|
|
34
34
|
|
|
35
35
|
`settled` flips true once the stored promise resolves. SSR snapshot
|
|
@@ -61,7 +61,7 @@ export type CacheEntry = {
|
|
|
61
61
|
ttl: number | undefined
|
|
62
62
|
expiresAt: number | undefined
|
|
63
63
|
value?: unknown
|
|
64
|
-
|
|
64
|
+
tags?: Set<string>
|
|
65
65
|
settled?: boolean
|
|
66
66
|
hydrated?: boolean
|
|
67
67
|
refreshing?: boolean
|
|
@@ -6,8 +6,8 @@ milliseconds-past-resolve that the entry stays live: omitted = forever, 0 =
|
|
|
6
6
|
dedupe only (entry dropped once the promise settles — the mutation idiom:
|
|
7
7
|
in-flight coalescing and pending() visibility, nothing retained), any other
|
|
8
8
|
number = TTL.
|
|
9
|
-
`
|
|
10
|
-
`cache.invalidate({
|
|
9
|
+
`tags` is one or more free-form labels grouping unrelated calls so one
|
|
10
|
+
`cache.invalidate({ tags })` drops every entry sharing any of them — pass an
|
|
11
11
|
array when a call belongs to multiple invalidation groups. A unique tag (e.g. a
|
|
12
12
|
uuid) shared by a set of calls gives them their own private invalidation group.
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ uncheckable — set `swr` only on a producer that is a pure read.
|
|
|
37
37
|
*/
|
|
38
38
|
export type CacheOptions = {
|
|
39
39
|
ttl?: number
|
|
40
|
-
|
|
40
|
+
tags?: string | string[]
|
|
41
41
|
global?: boolean
|
|
42
42
|
swr?: boolean | { throttle?: number; debounce?: number }
|
|
43
43
|
}
|
|
@@ -8,10 +8,10 @@ Selector grammar shared by cache.invalidate(), pending(), and refreshing():
|
|
|
8
8
|
remote fn → that function's calls (method+url prefix); fn and fn.raw match
|
|
9
9
|
the same set since they share wire identity
|
|
10
10
|
producer fn → that producer's calls (reference-id prefix)
|
|
11
|
-
{
|
|
11
|
+
{ tags } → any entry sharing one of the requested tags
|
|
12
12
|
*/
|
|
13
13
|
export type CacheSelector<Args, Return> =
|
|
14
14
|
| RemoteFunction<Args, Return>
|
|
15
15
|
| RawRemoteFunction<Args>
|
|
16
16
|
| ((args?: Args) => Promise<Return>)
|
|
17
|
-
| Pick<CacheOptions, '
|
|
17
|
+
| Pick<CacheOptions, 'tags'>
|
package/src/lib/shared/url.ts
CHANGED
|
@@ -15,7 +15,7 @@ verb, so url('/rpc/search', { q }) types its args against the verb's own
|
|
|
15
15
|
signature. Empty by default — an absent entry falls through to the page/asset
|
|
16
16
|
branch, so the helper works before the generated d.ts lands.
|
|
17
17
|
*/
|
|
18
|
-
// @
|
|
18
|
+
// @documentation url
|
|
19
19
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated rpc.d.ts
|
|
20
20
|
export interface RpcRoutes {}
|
|
21
21
|
|
|
@@ -12,7 +12,7 @@ the schema is declared:
|
|
|
12
12
|
Mutates and returns the same schema with the method attached, so the wrapped
|
|
13
13
|
value stays usable everywhere the bare schema was.
|
|
14
14
|
*/
|
|
15
|
-
// @
|
|
15
|
+
// @documentation rpc
|
|
16
16
|
export function withJsonSchema<Schema extends StandardSchemaV1>(
|
|
17
17
|
schema: Schema,
|
|
18
18
|
toJsonSchema: (schema: Schema) => Record<string, unknown>,
|
|
@@ -15,7 +15,7 @@ Returns the collected frames plus the `done` frame so a scenario can layer
|
|
|
15
15
|
its provider-specific assertions on top. This is the conformance seam: one
|
|
16
16
|
suite of invariants, N engine adapters run against it.
|
|
17
17
|
*/
|
|
18
|
-
// @
|
|
18
|
+
// @documentation plumbing
|
|
19
19
|
export async function assertAgentFrameConformance(
|
|
20
20
|
stream: AsyncIterable<AgentFrame>,
|
|
21
21
|
): Promise<{ frames: AgentFrame[]; done: Extract<AgentFrame, { type: 'done' }> }> {
|
|
@@ -17,7 +17,7 @@ surface out, with every `call` recorded so a test can assert exactly which
|
|
|
17
17
|
tools an engine dispatched and with what arguments. Prompts and resources are
|
|
18
18
|
empty — engines must tolerate an app exposing none.
|
|
19
19
|
*/
|
|
20
|
-
// @
|
|
20
|
+
// @documentation plumbing
|
|
21
21
|
export function createScriptedSurface(
|
|
22
22
|
tools: ScriptedTool[] = [],
|
|
23
23
|
): AgentSurface & { calls: RecordedCall[] } {
|
|
@@ -52,7 +52,7 @@ build's writeTestRpcDts / writeTestSocketsDts emit one entry per verb/socket
|
|
|
52
52
|
with no imports. Empty here; types arrive once the app has been built. Mirrors
|
|
53
53
|
url's RpcRoutes / health's AppHealthMap.
|
|
54
54
|
*/
|
|
55
|
-
// @
|
|
55
|
+
// @documentation testing
|
|
56
56
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated testRpc.d.ts
|
|
57
57
|
export interface RpcClient {}
|
|
58
58
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated testSockets.d.ts
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/* The callee names the `.abide` compiler recognises as reactive declarations
|
|
2
|
-
(`let x = state(...)`, `linked(...)`, `
|
|
2
|
+
(`let x = state(...)`, `linked(...)`, `computed(...)`, `prop(...)`): the shared
|
|
3
3
|
"is this a reactive binding" allowlist read by the desugarer, the nested-script
|
|
4
4
|
scoper, and the type-checking shadow. How each lowers — a serializable doc slot
|
|
5
5
|
vs a `.value` cell — is decided per-site; this is only the membership set, so a
|
|
6
6
|
new primitive is a single edit here. */
|
|
7
|
-
export const REACTIVE_CALLEES: ReadonlySet<string> = new Set([
|
|
7
|
+
export const REACTIVE_CALLEES: ReadonlySet<string> = new Set([
|
|
8
|
+
'state',
|
|
9
|
+
'linked',
|
|
10
|
+
'computed',
|
|
11
|
+
'prop',
|
|
12
|
+
])
|
|
@@ -8,10 +8,9 @@ that populates `window.__abide`. Order is the emit order.
|
|
|
8
8
|
export const UI_RUNTIME_IMPORTS: { name: string; specifier: string }[] = [
|
|
9
9
|
{ name: 'html', specifier: 'shared/html' },
|
|
10
10
|
{ name: 'snippet', specifier: 'shared/snippet' },
|
|
11
|
-
{ name: '
|
|
12
|
-
{ name: '
|
|
13
|
-
{ name: '
|
|
14
|
-
{ name: 'derived', specifier: 'ui/derived' },
|
|
11
|
+
{ name: 'scope', specifier: 'ui/scope' },
|
|
12
|
+
{ name: 'enterScope', specifier: 'ui/enterScope' },
|
|
13
|
+
{ name: 'exitScope', specifier: 'ui/exitScope' },
|
|
15
14
|
{ name: 'effect', specifier: 'ui/effect' },
|
|
16
15
|
{ name: 'mount', specifier: 'ui/dom/mount' },
|
|
17
16
|
{ name: 'appendText', specifier: 'ui/dom/appendText' },
|
|
@@ -29,7 +29,7 @@ that same linked sheet, so a page renders styled before the client bundle loads,
|
|
|
29
29
|
no inlined `<style>`. The elements still carry their `data-a-…` scope attributes
|
|
30
30
|
either way (one per `<style>` covering them — see `analyzeComponent`).
|
|
31
31
|
*/
|
|
32
|
-
// @
|
|
32
|
+
// @documentation plumbing
|
|
33
33
|
export const abideUiPlugin: BunPlugin = {
|
|
34
34
|
name: 'abide-ui',
|
|
35
35
|
setup(build) {
|
|
@@ -30,7 +30,7 @@ export function analyzeComponent(source: string, scopeSeed?: string): AnalyzedCo
|
|
|
30
30
|
const scriptBody = (scriptMatch?.[1] ?? '').trim()
|
|
31
31
|
const template = source.replace(/^\s*<script[^>]*>[\s\S]*?<\/script>/, '').trim()
|
|
32
32
|
|
|
33
|
-
const { code: desugared, stateNames, derivedNames } = desugarSignals(scriptBody)
|
|
33
|
+
const { code: desugared, stateNames, derivedNames, computedNames } = desugarSignals(scriptBody)
|
|
34
34
|
const lowered = desugared.trim() === '' ? '' : lowerDocAccess(desugared, 'model')
|
|
35
35
|
/* Hoist top-level import statements (e.g. child components) out of the script
|
|
36
36
|
so the module wrapper can place them at module scope — they can't live
|
|
@@ -54,6 +54,7 @@ export function analyzeComponent(source: string, scopeSeed?: string): AnalyzedCo
|
|
|
54
54
|
imports: imports.join('\n'),
|
|
55
55
|
stateNames,
|
|
56
56
|
derivedNames,
|
|
57
|
+
computedNames,
|
|
57
58
|
nodes,
|
|
58
59
|
styles,
|
|
59
60
|
/* Hydration adopts every block in place — including `await`, which resumes
|
|
@@ -7,7 +7,7 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
|
7
7
|
Compiles a single-file abide component into the body of a client build function.
|
|
8
8
|
Runs the shared front-end (`analyzeComponent`), generates the DOM build from the
|
|
9
9
|
template, and hoists static paths to cells. The returned body runs against a
|
|
10
|
-
`host` element with `doc`/`state`/`
|
|
10
|
+
`host` element with `doc`/`state`/`computed`/`effect` and the dom bindings in
|
|
11
11
|
scope and defines `model` itself. `compileModule` wraps it (and the SSR body) into
|
|
12
12
|
a real module; tests wrap it with `new Function`.
|
|
13
13
|
|
|
@@ -21,8 +21,8 @@ export function compileComponent(
|
|
|
21
21
|
scopeSeed?: string,
|
|
22
22
|
analyzed: AnalyzedComponent = analyzeComponent(source, scopeSeed),
|
|
23
23
|
): string {
|
|
24
|
-
const { script, stateNames, derivedNames, nodes } = analyzed
|
|
25
|
-
const build = generateBuild(nodes, 'host', stateNames, derivedNames, isLayout)
|
|
24
|
+
const { script, stateNames, derivedNames, computedNames, nodes } = analyzed
|
|
25
|
+
const build = generateBuild(nodes, 'host', stateNames, derivedNames, computedNames, isLayout)
|
|
26
26
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
27
27
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
28
28
|
elements, which `generateBuild` reads from each node's annotated `scopes`. */
|
|
@@ -93,6 +93,11 @@ ${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(
|
|
|
93
93
|
if (entry.name === 'mount' || entry.name === 'hydrate') {
|
|
94
94
|
return true
|
|
95
95
|
}
|
|
96
|
+
/* The SSR render always brackets itself with a per-render scope (compileSSR),
|
|
97
|
+
so these are referenced whenever the `render` export is — i.e. always. */
|
|
98
|
+
if (entry.name === 'enterScope' || entry.name === 'exitScope') {
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
96
101
|
/* Render-pass helpers are emitted by both back-ends (e.g. client and server
|
|
97
102
|
await/try blocks both call nextBlockId); their names are distinctive enough
|
|
98
103
|
that scanning both surfaces adds no false match. */
|
|
@@ -110,10 +115,33 @@ ${userImports}
|
|
|
110
115
|
${moduleBody}`
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
/* Indents a body block for embedding inside a wrapper function.
|
|
118
|
+
/* Indents a body block for embedding inside a wrapper function. Lines whose start
|
|
119
|
+
sits inside a multi-line template literal are left untouched — their leading
|
|
120
|
+
whitespace is significant string content (e.g. a CodeBlock's `code={`…`}`
|
|
121
|
+
snippet), and indenting it would corrupt the rendered sample. An odd count of
|
|
122
|
+
unescaped backticks on a line flips in/out of a literal. */
|
|
114
123
|
function indent(body: string): string {
|
|
124
|
+
let insideTemplateLiteral = false
|
|
115
125
|
return body
|
|
116
126
|
.split('\n')
|
|
117
|
-
.map((line) =>
|
|
127
|
+
.map((line) => {
|
|
128
|
+
const indented = insideTemplateLiteral || line === '' ? line : ` ${line}`
|
|
129
|
+
if (unescapedBacktickCount(line) % 2 === 1) {
|
|
130
|
+
insideTemplateLiteral = !insideTemplateLiteral
|
|
131
|
+
}
|
|
132
|
+
return indented
|
|
133
|
+
})
|
|
118
134
|
.join('\n')
|
|
119
135
|
}
|
|
136
|
+
|
|
137
|
+
/* Counts backticks not preceded by a backslash — the template-literal delimiters
|
|
138
|
+
on a line, ignoring escaped `\`` inside one. */
|
|
139
|
+
function unescapedBacktickCount(line: string): number {
|
|
140
|
+
let count = 0
|
|
141
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
142
|
+
if (line[index] === '`' && line[index - 1] !== '\\') {
|
|
143
|
+
count += 1
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return count
|
|
147
|
+
}
|
|
@@ -16,7 +16,7 @@ front-end, then the SSR back-end, and returns `{ html, state, awaits }`:
|
|
|
16
16
|
Effects are stripped — they are client lifecycle and emit no HTML, so the server
|
|
17
17
|
render is a snapshot of the markup before any effect runs.
|
|
18
18
|
|
|
19
|
-
Runs with `doc`/`state`/`
|
|
19
|
+
Runs with `doc`/`state`/`computed`/`effect`/`nextBlockId`/`enterRenderPass`/
|
|
20
20
|
`exitRenderPass` in scope and defines `model`. The body is bracketed by a render
|
|
21
21
|
pass so the outermost render resets the block-id counter and an inlined child
|
|
22
22
|
render continues it — keeping await/try ids unique and aligned with the client.
|
|
@@ -30,16 +30,16 @@ export function compileSSR(
|
|
|
30
30
|
scopeSeed?: string,
|
|
31
31
|
analyzed: AnalyzedComponent = analyzeComponent(source, scopeSeed),
|
|
32
32
|
): string {
|
|
33
|
-
const { script, stateNames, derivedNames, nodes } = analyzed
|
|
34
|
-
const ssr = generateSSR(nodes, stateNames, derivedNames, isLayout)
|
|
33
|
+
const { script, stateNames, derivedNames, computedNames, nodes } = analyzed
|
|
34
|
+
const ssr = generateSSR(nodes, stateNames, derivedNames, computedNames, isLayout)
|
|
35
35
|
/* No `<style>` in the markup — the scoped CSS is bundled into the entry stylesheet
|
|
36
36
|
the shell links (see `abideUiPlugin`), so SSR output is styled by that sheet. The
|
|
37
37
|
elements still carry their `data-a-…` scopes via `generateSSR`. */
|
|
38
38
|
/* `typeof model` guards a component with no reactive state (a pure-async or
|
|
39
39
|
static component declares no `model`); its snapshot is then empty. */
|
|
40
40
|
return (
|
|
41
|
-
`enterRenderPass();\ntry {\n${stripEffects(script)}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\n${ssr}` +
|
|
41
|
+
`enterRenderPass();\nconst $scope = enterScope();\ntry {\n${stripEffects(script)}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\n${ssr}` +
|
|
42
42
|
`return { html: $out.join(''), state: (typeof model !== 'undefined' ? model.snapshot() : {}), awaits: $awaits };\n` +
|
|
43
|
-
`} finally { exitRenderPass(); }`
|
|
43
|
+
`} finally { exitScope($scope); exitRenderPass(); }`
|
|
44
44
|
)
|
|
45
45
|
}
|