@abide/abide 0.38.0 → 0.39.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 +3 -3
- package/CHANGELOG.md +24 -0
- package/package.json +10 -1
- package/src/build.ts +14 -0
- package/src/lib/bundle/exitWithParent.ts +4 -2
- package/src/lib/server/rpc/readBodyWithinLimit.ts +3 -0
- package/src/lib/server/runtime/createAppAssetServer.ts +37 -11
- package/src/lib/server/runtime/createPublicAssetServer.ts +14 -5
- package/src/lib/server/runtime/createUiPageRenderer.ts +53 -42
- package/src/lib/server/runtime/internalErrorResponse.ts +5 -2
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +4 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +7 -0
- package/src/lib/shared/createRemoteFunction.ts +20 -11
- package/src/lib/shared/escapeHtml.ts +15 -0
- package/src/lib/shared/markFrameworkSourcesIgnored.ts +47 -0
- package/src/lib/shared/streamResponse.ts +8 -1
- package/src/lib/shared/types/RemoteCallable.ts +12 -3
- package/src/lib/shared/types/RpcOptions.ts +22 -0
- package/src/lib/shared/types/SourceMap.ts +14 -0
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +4 -3
- package/src/lib/ui/compile/SSR_ESCAPE.ts +13 -3
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +6 -0
- package/src/lib/ui/compile/asOutlet.ts +6 -5
- package/src/lib/ui/compile/compileModule.ts +18 -11
- package/src/lib/ui/compile/compileSSR.ts +32 -9
- package/src/lib/ui/compile/compileShadow.ts +11 -3
- package/src/lib/ui/compile/composeProps.ts +53 -0
- package/src/lib/ui/compile/desugarSignals.ts +45 -17
- package/src/lib/ui/compile/generateBuild.ts +100 -61
- package/src/lib/ui/compile/generateSSR.ts +226 -73
- 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/lowerDocAccess.ts +53 -16
- package/src/lib/ui/compile/parseTemplate.ts +44 -1
- package/src/lib/ui/compile/skeletonContext.ts +19 -20
- package/src/lib/ui/compile/skeletonable.ts +3 -2
- package/src/lib/ui/compile/spreadExcludedNames.ts +27 -0
- package/src/lib/ui/compile/staticAttr.ts +1 -1
- package/src/lib/ui/compile/staticTextPart.ts +1 -1
- package/src/lib/ui/compile/types/TemplateAttr.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +3 -1
- package/src/lib/ui/dom/commentData.ts +14 -0
- package/src/lib/ui/dom/disposeRange.ts +21 -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/mergeProps.ts +32 -0
- 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/outlet.ts +62 -0
- package/src/lib/ui/dom/readCall.ts +27 -0
- package/src/lib/ui/dom/restProps.ts +32 -0
- package/src/lib/ui/dom/scopeLabel.ts +8 -6
- package/src/lib/ui/dom/skeleton.ts +11 -30
- package/src/lib/ui/dom/spreadAttrs.ts +34 -0
- package/src/lib/ui/dom/spreadProps.ts +32 -0
- package/src/lib/ui/dom/withScope.ts +33 -0
- package/src/lib/ui/installHotBridge.ts +12 -0
- package/src/lib/ui/remoteProxy.ts +68 -36
- package/src/lib/ui/renderChain.ts +49 -39
- package/src/lib/ui/renderToStream.ts +69 -61
- package/src/lib/ui/resumeSeedScript.ts +17 -0
- package/src/lib/ui/router.ts +155 -85
- 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/createEffectNode.ts +5 -0
- package/src/lib/ui/runtime/hotReplace.ts +14 -10
- package/src/lib/ui/runtime/localStoragePersistence.ts +8 -1
- package/src/lib/ui/runtime/toTeardown.ts +10 -5
- package/src/lib/ui/runtime/types/HotInstance.ts +11 -7
- package/src/lib/ui/runtime/types/RenderContext.ts +8 -0
- package/src/lib/ui/runtime/types/Route.ts +6 -5
- package/src/lib/ui/runtime/types/SsrRender.ts +16 -11
- package/src/lib/ui/runtime/types/UiComponent.ts +12 -1
- package/src/lib/ui/compile/componentWrapperTag.ts +0 -15
- package/src/lib/ui/compile/escapeHtml.ts +0 -15
- package/src/lib/ui/runtime/firstOutlet.ts +0 -22
- /package/src/lib/{server/runtime → shared}/safeJsonForScript.ts +0 -0
|
@@ -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)
|
|
@@ -25,7 +25,14 @@ export function localStoragePersistence(): PersistenceStore | undefined {
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
save: (key, snapshot) => {
|
|
28
|
-
|
|
28
|
+
/* Swallow a failed write (QuotaExceededError, storage disabled mid-session) —
|
|
29
|
+
it fires from a debounced flush / pagehide handler with no caller to catch it,
|
|
30
|
+
and a dropped persist must not crash the app. */
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(key, JSON.stringify(snapshot))
|
|
33
|
+
} catch {
|
|
34
|
+
// best-effort persistence
|
|
35
|
+
}
|
|
29
36
|
},
|
|
30
37
|
remove: (key) => {
|
|
31
38
|
localStorage.removeItem(key)
|
|
@@ -15,11 +15,16 @@ export function toTeardown(result: EffectResult): Teardown | undefined {
|
|
|
15
15
|
}
|
|
16
16
|
if (result instanceof Promise) {
|
|
17
17
|
return () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
/* Swallow a rejection: an async body that rejected (e.g. an aborted RPC) must
|
|
19
|
+
not surface as an unhandled rejection when the teardown runs at dispose. */
|
|
20
|
+
result.then(
|
|
21
|
+
(teardown) => {
|
|
22
|
+
if (typeof teardown === 'function') {
|
|
23
|
+
teardown()
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
() => undefined,
|
|
27
|
+
)
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
return undefined
|
|
@@ -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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* The per-request SSR render context, threaded through a layout chain and every
|
|
2
|
+
child component a page inlines. `next` is the block-id counter: each `await`/`try`
|
|
3
|
+
block draws from it in depth-first document order — the SAME order the client
|
|
4
|
+
allocates ids during its synchronous hydration walk, so the streamed fragments and
|
|
5
|
+
the `RESUME` manifest line up. Request-local (not a module global) because SSR
|
|
6
|
+
render is async — a blocking `await` yields, and a shared global counter would
|
|
7
|
+
interleave across concurrent requests. */
|
|
8
|
+
export type RenderContext = { next: number }
|
|
@@ -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
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { ResumeEntry } from '../RESUME.ts'
|
|
2
|
+
|
|
3
|
+
/* One STREAMING await block captured during SSR (no `then` on the `await` tag): its
|
|
4
|
+
boundary id, the promise to await, and the async string-renderers for the resolved
|
|
5
|
+
value / error. `renderToStream` flushes each resolved fragment out of order. The
|
|
6
|
+
renderers are async so a nested `await` block inside the branch composes. `catch`
|
|
7
|
+
is absent when the block has no catch branch — a rejection then surfaces to the
|
|
8
|
+
stream/error path instead of rendering an empty branch. (Blocking awaits — a `then`
|
|
9
|
+
on the tag — never land here: they render inline during the async render pass and
|
|
10
|
+
seed `SsrRender.resume`.) */
|
|
7
11
|
export type SsrAwait = {
|
|
8
12
|
id: number
|
|
9
|
-
blocking?: boolean
|
|
10
13
|
promise: () => unknown
|
|
11
|
-
then: (value: unknown) => string
|
|
12
|
-
catch?: (error: unknown) => string
|
|
14
|
+
then: (value: unknown) => Promise<string>
|
|
15
|
+
catch?: (error: unknown) => Promise<string>
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/* The result of a component's server `render()`: the pending-shell HTML, the
|
|
16
|
-
serializable document snapshot for client resume,
|
|
17
|
-
|
|
19
|
+
serializable document snapshot for client resume, the STREAMING await blocks to
|
|
20
|
+
flush out of order, and `resume` — the inline-rendered BLOCKING await values keyed
|
|
21
|
+
by boundary id, seeded into the manifest so hydration adopts them without a refetch. */
|
|
18
22
|
export type SsrRender = {
|
|
19
23
|
html: string
|
|
20
24
|
state: unknown
|
|
21
25
|
awaits: SsrAwait[]
|
|
26
|
+
resume: Record<number, ResumeEntry>
|
|
22
27
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RenderContext } from './RenderContext.ts'
|
|
1
2
|
import type { SsrRender } from './SsrRender.ts'
|
|
2
3
|
import type { UiProps } from './UiProps.ts'
|
|
3
4
|
|
|
@@ -7,8 +8,18 @@ for SSR and the hydration hooks. This is the shape `compileModule` emits and the
|
|
|
7
8
|
page/route registries carry — abide-ui's compiled-component shape.
|
|
8
9
|
*/
|
|
9
10
|
export type UiComponent = ((host: Element, props?: UiProps) => () => void) & {
|
|
10
|
-
|
|
11
|
+
/* `ctx` is the request-local block-id counter, threaded so a child's ids share the
|
|
12
|
+
page's depth-first numbering; omitted at the top level (a fresh counter defaults
|
|
13
|
+
in). Returns a Promise when the component has an inline `await` (a blocking `{#await
|
|
14
|
+
… then}` block or a child render); otherwise renders synchronously. Callers `await`
|
|
15
|
+
it either way (awaiting a sync value just returns it). */
|
|
16
|
+
render: (props?: UiProps, ctx?: RenderContext) => SsrRender | Promise<SsrRender>
|
|
11
17
|
hydrate?: (host: Element, props?: UiProps) => () => void
|
|
18
|
+
/* The bare client build (`(host, props) => void`) — appends the component's nodes
|
|
19
|
+
to `host`. A nested child mounts it into a marker range (`mountRange`/`mountChild`)
|
|
20
|
+
instead of the wrapped `mount`, and `hotReplace` re-fills a range with the new
|
|
21
|
+
module's `build` on edit. */
|
|
22
|
+
build: (host: Node, props?: UiProps) => void
|
|
12
23
|
hydratable?: boolean
|
|
13
24
|
/* Stable module id (project-relative source path) stamped by `compileModule`,
|
|
14
25
|
keying the component in the hot-reload registry — see `mountChild`. */
|
|
@@ -1,15 +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
|
-
import { COMPONENT_WRAPPER_PREFIX } from '../COMPONENT_WRAPPER_PREFIX.ts'
|
|
12
|
-
|
|
13
|
-
export function componentWrapperTag(name: string): string {
|
|
14
|
-
return `${COMPONENT_WRAPPER_PREFIX}${name.toLowerCase()}`
|
|
15
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
HTML-escapes a compile-time-constant string — a static attribute value or static
|
|
3
|
-
text — the same five characters the runtime `$esc` handles. Shared by the SSR
|
|
4
|
-
generator and the static-clone skeleton generator so server markup and the client
|
|
5
|
-
clone template can't diverge on escaping. Static text reaches here already
|
|
6
|
-
entity-decoded (see parseTemplate), so escaping round-trips it through the HTML
|
|
7
|
-
parser to the same plain text the client would build directly.
|
|
8
|
-
*/
|
|
9
|
-
export function escapeHtml(value: string): string {
|
|
10
|
-
return value.replace(
|
|
11
|
-
/[&<>"']/g,
|
|
12
|
-
(char) =>
|
|
13
|
-
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char] ?? char,
|
|
14
|
-
)
|
|
15
|
-
}
|
|
@@ -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
|
-
}
|
|
File without changes
|