@abide/abide 0.37.0 → 0.38.1
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 +1 -1
- package/CHANGELOG.md +38 -0
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +3 -17
- package/src/appEntry.ts +18 -8
- package/src/controlServerWorker.ts +8 -1
- package/src/lib/cli/parseArgvForRpc.ts +8 -3
- package/src/lib/mcp/createMcpServer.ts +8 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +20 -2
- package/src/lib/mcp/toolResultFromResponse.ts +5 -0
- package/src/lib/server/rpc/parseArgs.ts +12 -1
- package/src/lib/server/runtime/acceptsGzip.ts +10 -1
- package/src/lib/server/runtime/gzipResponse.ts +2 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -1
- package/src/lib/server/runtime/streamFromIterator.ts +8 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +7 -1
- package/src/lib/server/sockets/defineSocket.ts +6 -1
- package/src/lib/shared/cache.ts +6 -0
- package/src/lib/shared/createPushIterator.ts +7 -2
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +5 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/asOutlet.ts +30 -0
- package/src/lib/ui/compile/compileModule.ts +13 -9
- package/src/lib/ui/compile/generateBuild.ts +83 -72
- package/src/lib/ui/compile/generateSSR.ts +56 -37
- package/src/lib/ui/compile/isAnchorPositioned.ts +19 -0
- package/src/lib/ui/compile/isControlFlow.ts +3 -1
- package/src/lib/ui/compile/isTextLeaf.ts +1 -1
- package/src/lib/ui/compile/parseTemplate.ts +10 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -2
- package/src/lib/ui/compile/skeletonContext.ts +97 -35
- package/src/lib/ui/compile/skeletonable.ts +3 -2
- package/src/lib/ui/compile/types/SkeletonContext.ts +6 -0
- package/src/lib/ui/dom/appendSnippet.ts +60 -20
- package/src/lib/ui/dom/commentData.ts +14 -0
- package/src/lib/ui/dom/disposeRange.ts +21 -0
- package/src/lib/ui/dom/fillBefore.ts +10 -0
- package/src/lib/ui/dom/fillBoundary.ts +38 -0
- package/src/lib/ui/dom/fillRange.ts +30 -0
- package/src/lib/ui/dom/hydrate.ts +11 -21
- package/src/lib/ui/dom/mount.ts +16 -25
- package/src/lib/ui/dom/mountChild.ts +27 -14
- package/src/lib/ui/dom/mountRange.ts +45 -0
- package/src/lib/ui/dom/mountSlot.ts +7 -2
- package/src/lib/ui/dom/outlet.ts +62 -0
- package/src/lib/ui/dom/scopeLabel.ts +13 -7
- package/src/lib/ui/dom/skeleton.ts +11 -27
- package/src/lib/ui/dom/withScope.ts +33 -0
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/persist.ts +4 -1
- package/src/lib/ui/renderChain.ts +23 -15
- package/src/lib/ui/router.ts +78 -38
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +10 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +5 -6
- package/src/lib/ui/runtime/PENDING_OUTLET.ts +8 -0
- package/src/lib/ui/runtime/captureModelDoc.ts +12 -13
- package/src/lib/ui/runtime/createDoc.ts +20 -7
- package/src/lib/ui/runtime/hotReplace.ts +14 -10
- package/src/lib/ui/runtime/types/HotInstance.ts +11 -7
- package/src/lib/ui/runtime/types/Route.ts +6 -5
- package/src/lib/ui/runtime/types/UiComponent.ts +5 -0
- package/src/lib/ui/seedStreamedResolution.ts +10 -1
- package/src/lib/ui/compile/componentWrapperTag.ts +0 -13
- package/src/lib/ui/runtime/firstOutlet.ts +0 -22
|
@@ -2,28 +2,27 @@ import { PATCH_BUS } from './PATCH_BUS.ts'
|
|
|
2
2
|
import type { Doc } from './types/Doc.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
Runs a component `build` and returns its
|
|
6
|
-
`model` document — the serializable `state` doc,
|
|
7
|
-
its value across (see `hotReplace`). The model is
|
|
8
|
-
seeds its `model` first (the desugared
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
preserve. Used only on the hot path; the
|
|
5
|
+
Runs a component `build` and returns its result (the mount handle / disposer)
|
|
6
|
+
alongside the component's own `model` document — the serializable `state` doc,
|
|
7
|
+
needed so a hot swap can carry its value across (see `hotReplace`). The model is
|
|
8
|
+
found, not threaded: a component seeds its `model` first (the desugared
|
|
9
|
+
`const model = doc({})` + its init patches run before any child mounts or
|
|
10
|
+
control-flow blocks), so the FIRST patch announced on the bus during the build names
|
|
11
|
+
it. A component with no `state` mints no model and emits nothing first — `model` is
|
|
12
|
+
then `undefined` and there is nothing to preserve. Used only on the hot path; the
|
|
13
|
+
subscription is torn down with the build.
|
|
13
14
|
*/
|
|
14
|
-
export function captureModelDoc(build: () =>
|
|
15
|
-
|
|
15
|
+
export function captureModelDoc<T>(build: () => T): {
|
|
16
|
+
value: T
|
|
16
17
|
model: Doc | undefined
|
|
17
18
|
} {
|
|
18
19
|
let model: Doc | undefined
|
|
19
20
|
const unsubscribe = PATCH_BUS.subscribe((event) => {
|
|
20
21
|
model ??= event.doc
|
|
21
22
|
})
|
|
22
|
-
let dispose: () => void = () => undefined
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
return { value: build(), model }
|
|
25
25
|
} finally {
|
|
26
26
|
unsubscribe()
|
|
27
27
|
}
|
|
28
|
-
return { dispose, model }
|
|
29
28
|
}
|
|
@@ -14,6 +14,13 @@ import { unescapeKey } from './unescapeKey.ts'
|
|
|
14
14
|
import { walkPath } from './walkPath.ts'
|
|
15
15
|
import { writeNode } from './writeNode.ts'
|
|
16
16
|
|
|
17
|
+
/* `path` minus its last segment — the parent container's path, '' at the root.
|
|
18
|
+
The same string `segments.slice(0, -1).join('/')` rebuilds, by one slice. */
|
|
19
|
+
function parentPathOf(path: string): string {
|
|
20
|
+
const lastSlash = path.lastIndexOf('/')
|
|
21
|
+
return lastSlash === -1 ? '' : path.slice(0, lastSlash)
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
/*
|
|
18
25
|
Builds a reactive document over `initial`. Each path read for the first time
|
|
19
26
|
mints a signal node; the node is the notification token, the (mutable) tree is
|
|
@@ -132,10 +139,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
132
139
|
remove (computed post-apply, below, to resolve an array append's index). */
|
|
133
140
|
const before = PATCH_BUS.active ? walkPath(tree, patch.path) : undefined
|
|
134
141
|
tree = applyPatchToTree(tree, patch, segments)
|
|
135
|
-
|
|
136
|
-
`segments.slice(0, -1).join('/')` rebuilds, taken by one slice instead. */
|
|
137
|
-
const lastSlash = patch.path.lastIndexOf('/')
|
|
138
|
-
const parentPath = lastSlash === -1 ? '' : patch.path.slice(0, lastSlash)
|
|
142
|
+
const parentPath = parentPathOf(patch.path)
|
|
139
143
|
const parentValue = walkPath(tree, parentPath).value
|
|
140
144
|
const leafKey = segments[segments.length - 1] as string | undefined
|
|
141
145
|
/* A structural change (add/remove, or an array element replaced by index)
|
|
@@ -194,8 +198,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
194
198
|
before: ReturnType<typeof walkPath> | undefined,
|
|
195
199
|
): Patch | undefined {
|
|
196
200
|
if (patch.op === 'add') {
|
|
197
|
-
const
|
|
198
|
-
const parentPath = lastSlash === -1 ? '' : patch.path.slice(0, lastSlash)
|
|
201
|
+
const parentPath = parentPathOf(patch.path)
|
|
199
202
|
const parent = walkPath(tree, parentPath).value
|
|
200
203
|
const resolved =
|
|
201
204
|
Array.isArray(parent) && patch.path.endsWith('/-')
|
|
@@ -223,9 +226,19 @@ export function createDoc(initial: unknown): Doc {
|
|
|
223
226
|
const node = nodeFor(path)
|
|
224
227
|
const segments = path.split('/').map(unescapeKey)
|
|
225
228
|
const leafKey = segments[segments.length - 1] as string
|
|
229
|
+
/* Auto-vivify missing ancestor objects so binding a nested path on a doc
|
|
230
|
+
booted shallow (e.g. `state({})`) doesn't crash, and a later `set` writes
|
|
231
|
+
into the LIVE tree (so snapshot/persist see it). Mirrors the container
|
|
232
|
+
assumption applyPatchToTree makes — except the patch path is authored, this
|
|
233
|
+
walk is compiler-emitted, so the intermediates may not exist yet. */
|
|
226
234
|
let parent = tree as Record<string, unknown>
|
|
227
235
|
for (const segment of segments.slice(0, -1)) {
|
|
228
|
-
|
|
236
|
+
let next = parent[segment]
|
|
237
|
+
if (next === null || typeof next !== 'object') {
|
|
238
|
+
next = {}
|
|
239
|
+
parent[segment] = next
|
|
240
|
+
}
|
|
241
|
+
parent = next as Record<string, unknown>
|
|
229
242
|
}
|
|
230
243
|
return {
|
|
231
244
|
get: () => readNode(node) as T,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fillRange } from '../dom/fillRange.ts'
|
|
1
2
|
import { captureModelDoc } from './captureModelDoc.ts'
|
|
2
3
|
import { hotInstances } from './hotInstances.ts'
|
|
3
4
|
import { seedModelDoc } from './seedModelDoc.ts'
|
|
@@ -5,14 +6,15 @@ import type { UiComponent } from './types/UiComponent.ts'
|
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
Swaps every live instance of an edited component to its new factory. Per instance:
|
|
8
|
-
snapshot its model, dispose the current scope and its DOM (the
|
|
9
|
-
the
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
live signals). The hot module
|
|
13
|
-
|
|
14
|
-
none mounted — e.g. a
|
|
15
|
-
|
|
9
|
+
snapshot its model, dispose the current scope and clear its DOM range (the disposer
|
|
10
|
+
leaves the `start`/`end` markers in place), re-fill the SAME range with the new
|
|
11
|
+
module's `build` and the same props, then re-seed the fresh model from the snapshot —
|
|
12
|
+
so the user's in-progress `state` survives the edit (state above the boundary already
|
|
13
|
+
survives: props are thunks re-reading the parent's live signals). The hot module
|
|
14
|
+
calls this on load with its freshly compiled `next`. Returns whether it swapped at
|
|
15
|
+
least one instance: false (the edited component has none mounted — e.g. a
|
|
16
|
+
router-mounted page, or a hidden branch) tells the caller to fall back to a full
|
|
17
|
+
reload, since nothing on screen would update.
|
|
16
18
|
*/
|
|
17
19
|
export function hotReplace(moduleId: string, next: UiComponent): boolean {
|
|
18
20
|
const set = hotInstances.get(moduleId)
|
|
@@ -23,8 +25,10 @@ export function hotReplace(moduleId: string, next: UiComponent): boolean {
|
|
|
23
25
|
const saved = instance.model?.snapshot()
|
|
24
26
|
instance.dispose()
|
|
25
27
|
instance.factory = next
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
+
const { value: handle, model } = captureModelDoc(() =>
|
|
29
|
+
fillRange(instance.start, instance.end, next.build, instance.props, instance.label),
|
|
30
|
+
)
|
|
31
|
+
instance.dispose = handle.dispose
|
|
28
32
|
instance.model = model
|
|
29
33
|
if (saved !== undefined && model !== undefined) {
|
|
30
34
|
seedModelDoc(model, saved)
|
|
@@ -2,15 +2,19 @@ import type { Doc } from './Doc.ts'
|
|
|
2
2
|
import type { UiComponent } from './UiComponent.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
A live component instance the hot-reload registry tracks:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
A live component instance the hot-reload registry tracks: the `start`/`end` markers
|
|
6
|
+
bounding its DOM range (the wrapper-free mount, see `mountRange`), its `label` (the
|
|
7
|
+
scope name), the factory that built it, the props (thunks re-read on a swap so the
|
|
8
|
+
parent's live state still flows through), the disposer for its current scope + DOM,
|
|
9
|
+
and its own `model` document (`undefined` when the component has no `state`) —
|
|
10
|
+
snapshotted before a swap and re-seeded after, so the user's in-progress state
|
|
11
|
+
survives the edit. A swap re-fills the SAME range and mutates `factory`/`dispose`/
|
|
12
|
+
`model` in place (see `hotReplace`).
|
|
11
13
|
*/
|
|
12
14
|
export type HotInstance = {
|
|
13
|
-
|
|
15
|
+
start: Comment
|
|
16
|
+
end: Comment
|
|
17
|
+
label: string | undefined
|
|
14
18
|
factory: UiComponent
|
|
15
19
|
props: Parameters<UiComponent>[1]
|
|
16
20
|
dispose: () => void
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/* A routable page
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
the page has an `await` block)
|
|
1
|
+
/* A routable page/layout. Callable to mount directly into a host (the direct-mount
|
|
2
|
+
API), but the router instead uses `build` — the bare client build — to fill the
|
|
3
|
+
layer into its outlet boundary as a marker range (no `<abide-outlet>` wrapper; see
|
|
4
|
+
`fillBoundary`/`outlet`). `hydratable` (false when the page has an `await` block)
|
|
5
|
+
tells the router whether the first paint adopts the SSR DOM in place. */
|
|
5
6
|
export type Route = ((host: Element, props?: unknown) => (() => void) | undefined) & {
|
|
6
|
-
|
|
7
|
+
build: (host: Node, props?: unknown) => void
|
|
7
8
|
hydratable?: boolean
|
|
8
9
|
}
|
|
@@ -9,6 +9,11 @@ page/route registries carry — abide-ui's compiled-component shape.
|
|
|
9
9
|
export type UiComponent = ((host: Element, props?: UiProps) => () => void) & {
|
|
10
10
|
render: (props?: UiProps) => SsrRender
|
|
11
11
|
hydrate?: (host: Element, props?: UiProps) => () => void
|
|
12
|
+
/* The bare client build (`(host, props) => void`) — appends the component's nodes
|
|
13
|
+
to `host`. A nested child mounts it into a marker range (`mountRange`/`mountChild`)
|
|
14
|
+
instead of the wrapped `mount`, and `hotReplace` re-fills a range with the new
|
|
15
|
+
module's `build` on edit. */
|
|
16
|
+
build: (host: Node, props?: UiProps) => void
|
|
12
17
|
hydratable?: boolean
|
|
13
18
|
/* Stable module id (project-relative source path) stamped by `compileModule`,
|
|
14
19
|
keying the component in the hot-reload registry — see `mountChild`. */
|
|
@@ -18,5 +18,14 @@ export function seedStreamedResolution(resolution: StreamedResolution): void {
|
|
|
18
18
|
if ('miss' in resolution) {
|
|
19
19
|
return
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
/* Only seed when nothing live holds the key — or when the existing entry is itself
|
|
22
|
+
an unconsumed hydrated seed (`hydrated === true`, cleared by the first cache()
|
|
23
|
+
read). A live/settled non-hydrated entry is authoritative; clobbering it with a
|
|
24
|
+
stale snapshot would drop a fresher value (e.g. one a live fetch already wrote). */
|
|
25
|
+
const { entries } = activeCacheStore()
|
|
26
|
+
const existing = entries.get(resolution.key)
|
|
27
|
+
if (existing !== undefined && existing.hydrated !== true) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
entries.set(resolution.key, cacheEntryFromSnapshot(resolution))
|
|
22
31
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
The element tag a component instance mounts into: always `abide-<name>` lowercased. The
|
|
3
|
-
`abide-` prefix makes every wrapper a valid custom element (contains a hyphen) — never
|
|
4
|
-
void, no content model — so it holds the component's own markup untouched and hydrates
|
|
5
|
-
cleanly, regardless of whether the name collides with an HTML element (`Button`, `Input`).
|
|
6
|
-
Emitted with `display:contents` (see the back-ends) so the wrapper stays out of layout: a
|
|
7
|
-
pure mount host whose real root lays out as a direct child of the parent, keeping the
|
|
8
|
-
component invisible to `grid`/`subgrid`/`flex`. Both back-ends call this so the SSR string
|
|
9
|
-
and the client build agree on the wrapper.
|
|
10
|
-
*/
|
|
11
|
-
export function componentWrapperTag(name: string): string {
|
|
12
|
-
return `abide-${name.toLowerCase()}`
|
|
13
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { OUTLET_TAG } from './OUTLET_TAG.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
The first `<abide-outlet>` in `root`'s subtree, in document order — the position
|
|
5
|
-
the router fills with the next layer of a route's layout chain. A depth-first walk
|
|
6
|
-
over `children` rather than `querySelector`: the router runs against real DOM and
|
|
7
|
-
the test mini-DOM alike, and only the former has `querySelector`. Tag comparison is
|
|
8
|
-
case-insensitive (the real DOM uppercases `tagName`, the mini-DOM keeps it as
|
|
9
|
-
created). Returns undefined when the layout declares no `<slot/>`.
|
|
10
|
-
*/
|
|
11
|
-
export function firstOutlet(root: Element): Element | undefined {
|
|
12
|
-
for (const child of root.children) {
|
|
13
|
-
if (child.tagName.toLowerCase() === OUTLET_TAG) {
|
|
14
|
-
return child
|
|
15
|
-
}
|
|
16
|
-
const nested = firstOutlet(child)
|
|
17
|
-
if (nested !== undefined) {
|
|
18
|
-
return nested
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return undefined
|
|
22
|
-
}
|