@abide/abide 0.38.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 +8 -0
- package/package.json +2 -1
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +4 -3
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/asOutlet.ts +6 -5
- package/src/lib/ui/compile/compileModule.ts +13 -9
- package/src/lib/ui/compile/generateBuild.ts +55 -44
- package/src/lib/ui/compile/generateSSR.ts +30 -21
- 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/skeletonContext.ts +19 -20
- package/src/lib/ui/compile/skeletonable.ts +3 -2
- 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/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/scopeLabel.ts +8 -6
- package/src/lib/ui/dom/skeleton.ts +11 -30
- package/src/lib/ui/dom/withScope.ts +33 -0
- package/src/lib/ui/installHotBridge.ts +2 -0
- 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/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/compile/componentWrapperTag.ts +0 -15
- package/src/lib/ui/runtime/firstOutlet.ts +0 -22
package/src/lib/ui/router.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { layoutChainForRoute } from '../shared/layoutChainForRoute.ts'
|
|
2
|
+
import { fillBoundary } from './dom/fillBoundary.ts'
|
|
3
|
+
import { outlet } from './dom/outlet.ts'
|
|
2
4
|
import { effect } from './effect.ts'
|
|
3
5
|
import { matchRoute } from './matchRoute.ts'
|
|
4
6
|
import { navigate } from './navigate.ts'
|
|
5
7
|
import { clientPage } from './runtime/clientPage.ts'
|
|
6
8
|
import { enterRenderPass } from './runtime/enterRenderPass.ts'
|
|
7
9
|
import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
8
|
-
import { firstOutlet } from './runtime/firstOutlet.ts'
|
|
9
10
|
import { historyEntries } from './runtime/historyEntries.ts'
|
|
11
|
+
import { PENDING_OUTLET } from './runtime/PENDING_OUTLET.ts'
|
|
12
|
+
import { RENDER } from './runtime/RENDER.ts'
|
|
10
13
|
import { runtimePath } from './runtime/runtimePath.ts'
|
|
11
14
|
import type { AbideHistoryState } from './runtime/types/AbideHistoryState.ts'
|
|
12
15
|
import type { NavVerdict } from './runtime/types/NavVerdict.ts'
|
|
@@ -14,13 +17,15 @@ import type { Route } from './runtime/types/Route.ts'
|
|
|
14
17
|
import type { RouteLoader } from './runtime/types/RouteLoader.ts'
|
|
15
18
|
import { untrack } from './runtime/untrack.ts'
|
|
16
19
|
|
|
17
|
-
/*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type
|
|
20
|
+
/* An outlet boundary — the `<!--abide:outlet-->`…`<!--/abide:outlet-->` marker pair a
|
|
21
|
+
layer's content lives between (a layout's `<slot/>`, or the router's root boundary in
|
|
22
|
+
the mount host). The next chain layer fills it; no `<abide-outlet>` element. */
|
|
23
|
+
type Boundary = { open: Comment; close: Comment }
|
|
21
24
|
|
|
22
|
-
/* A layout
|
|
23
|
-
|
|
25
|
+
/* A layout mounted in the active chain: its route key (the directory URL — its identity
|
|
26
|
+
for the diff), the disposer that stops its reactivity and clears its content, and the
|
|
27
|
+
boundary of its own `<slot/>` — where the next layer mounts. */
|
|
28
|
+
type MountedLayout = { key: string; dispose: () => void; slot: Boundary }
|
|
24
29
|
|
|
25
30
|
/* The destination URL for a navigation `path`. On the server / headless there is no
|
|
26
31
|
`location`, so resolve against a localhost origin; in the browser, against the real
|
|
@@ -68,6 +73,10 @@ export function router(
|
|
|
68
73
|
/* The mounted layout chain (outermost first) + the page disposer. */
|
|
69
74
|
const mountedLayouts: MountedLayout[] = []
|
|
70
75
|
let disposePage: (() => void) | undefined
|
|
76
|
+
/* The root outlet boundary in `host` (`#app`) the outermost layer fills — established
|
|
77
|
+
once on the first mount (claimed from the SSR DOM when hydrating, created otherwise)
|
|
78
|
+
and reused across navigations, since `#app` itself never re-mounts. */
|
|
79
|
+
let rootBoundary: Boundary | undefined
|
|
71
80
|
const patterns = Object.keys(loaders).filter((key) => key !== '*')
|
|
72
81
|
const layoutKeys = Object.keys(layoutLoaders)
|
|
73
82
|
|
|
@@ -90,60 +99,94 @@ export function router(
|
|
|
90
99
|
const resolvePage = resolver(loaders)
|
|
91
100
|
const resolveLayout = resolver(layoutLoaders)
|
|
92
101
|
|
|
93
|
-
/* Tear down the page and every layout from `index` inward (innermost first)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
/* Tear down the page and every layout from `index` inward (innermost first). Each
|
|
103
|
+
layer's disposer stops its reactivity and clears its content from its boundary
|
|
104
|
+
(the outermost cleared range removes the inner DOM too — harmless double-clears).
|
|
105
|
+
Leaves the boundary markers in place so the rebuild fills the same boundary. */
|
|
106
|
+
const disposeFrom = (index: number): void => {
|
|
97
107
|
disposePage?.()
|
|
98
108
|
disposePage = undefined
|
|
99
109
|
for (let depth = mountedLayouts.length - 1; depth >= index; depth -= 1) {
|
|
100
110
|
mountedLayouts[depth]?.dispose()
|
|
101
111
|
}
|
|
102
|
-
const base = index === 0 ? host : (mountedLayouts[index - 1] as MountedLayout).outlet
|
|
103
112
|
mountedLayouts.length = index
|
|
104
|
-
return base
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
/*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
/* The outlet boundary the layer at `index` fills: the root boundary for the outermost
|
|
116
|
+
layer, else the surviving parent layout's `<slot/>`. */
|
|
117
|
+
const baseBoundary = (index: number): Boundary =>
|
|
118
|
+
index === 0 ? (rootBoundary as Boundary) : (mountedLayouts[index - 1] as MountedLayout).slot
|
|
119
|
+
|
|
120
|
+
/* Build (or hydrate) the chain tail — layouts `[index..]` then the page — filling each
|
|
121
|
+
layer into the previous one's `<slot/>` boundary (the root boundary for the
|
|
122
|
+
outermost). `outlet()` records each layer's own slot in `PENDING_OUTLET` as the
|
|
123
|
+
layer builds, so the next layer knows where to mount — no DOM scan. Hydration
|
|
124
|
+
brackets ONE render pass + claim cursor across all layers so await/try block ids
|
|
125
|
+
stay unique and aligned with the SSR stream; a fresh mount needs no shared pass. */
|
|
111
126
|
const buildFrom = (
|
|
112
|
-
base: Element,
|
|
113
127
|
index: number,
|
|
114
128
|
chainKeys: string[],
|
|
115
129
|
layoutViews: Route[],
|
|
116
130
|
pageView: Route | undefined,
|
|
131
|
+
pageKey: string,
|
|
117
132
|
params: Record<string, string>,
|
|
118
133
|
hydrating: boolean,
|
|
119
134
|
): void => {
|
|
120
135
|
const run = (): void => {
|
|
121
|
-
|
|
136
|
+
/* Establish the root boundary on the first mount — `outlet(host)` claims the
|
|
137
|
+
SSR root boundary (hydrating) or creates it (fresh), recording it in
|
|
138
|
+
`PENDING_OUTLET`. Reused across navigations thereafter. A fresh first mount
|
|
139
|
+
(no claim cursor — e.g. a non-hydratable page whose SSR shell can't be
|
|
140
|
+
adopted) discards whatever the server put in `#app` first, so the created
|
|
141
|
+
boundary is the only content. */
|
|
142
|
+
if (rootBoundary === undefined) {
|
|
143
|
+
if (RENDER.hydration === undefined) {
|
|
144
|
+
host.textContent = ''
|
|
145
|
+
}
|
|
146
|
+
outlet(host)
|
|
147
|
+
rootBoundary = PENDING_OUTLET.current as Boundary
|
|
148
|
+
}
|
|
149
|
+
let boundary = baseBoundary(index)
|
|
122
150
|
for (let depth = index; depth < layoutViews.length; depth += 1) {
|
|
123
151
|
const view = layoutViews[depth] as Route
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
PENDING_OUTLET.current = undefined
|
|
153
|
+
const { dispose } = fillBoundary(
|
|
154
|
+
boundary.open,
|
|
155
|
+
boundary.close,
|
|
156
|
+
view.build,
|
|
157
|
+
params,
|
|
158
|
+
/* The layout's route key names its scope in the inspector's Reactive tab
|
|
159
|
+
(no host element to read a tag from — see `scopeLabel`). */
|
|
160
|
+
chainKeys[depth],
|
|
161
|
+
)
|
|
162
|
+
const slot = PENDING_OUTLET.current
|
|
163
|
+
if (slot === undefined) {
|
|
129
164
|
throw new Error('[abide] a layout.abide must contain a <slot/> outlet')
|
|
130
165
|
}
|
|
131
|
-
mountedLayouts.push({ key: chainKeys[depth] as string, dispose,
|
|
132
|
-
|
|
166
|
+
mountedLayouts.push({ key: chainKeys[depth] as string, dispose, slot })
|
|
167
|
+
boundary = slot
|
|
133
168
|
}
|
|
134
169
|
if (pageView === undefined) {
|
|
135
170
|
return
|
|
136
171
|
}
|
|
137
|
-
disposePage =
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
disposePage = fillBoundary(
|
|
173
|
+
boundary.open,
|
|
174
|
+
boundary.close,
|
|
175
|
+
pageView.build,
|
|
176
|
+
params,
|
|
177
|
+
/* The page's route key names its scope in the inspector (see above). */
|
|
178
|
+
pageKey,
|
|
179
|
+
).dispose
|
|
140
180
|
}
|
|
141
181
|
if (hydrating) {
|
|
182
|
+
const previous = RENDER.hydration
|
|
183
|
+
RENDER.hydration = { next: new Map() }
|
|
142
184
|
enterRenderPass()
|
|
143
185
|
try {
|
|
144
186
|
run()
|
|
145
187
|
} finally {
|
|
146
188
|
exitRenderPass()
|
|
189
|
+
RENDER.hydration = previous
|
|
147
190
|
}
|
|
148
191
|
return
|
|
149
192
|
}
|
|
@@ -329,11 +372,11 @@ export function router(
|
|
|
329
372
|
) {
|
|
330
373
|
divergence += 1
|
|
331
374
|
}
|
|
332
|
-
const hydrating =
|
|
333
|
-
first && pageView?.hydratable === true && pageView.hydrate !== undefined
|
|
375
|
+
const hydrating = first && pageView?.hydratable === true
|
|
334
376
|
first = false
|
|
335
|
-
/* The DOM mutation a navigation makes: tear the divergent chain down
|
|
336
|
-
|
|
377
|
+
/* The DOM mutation a navigation makes: tear the divergent chain down
|
|
378
|
+
(clearing its content from its boundary) and rebuild into the same
|
|
379
|
+
boundary (hydration adopts in place). */
|
|
337
380
|
const swap = (): void => {
|
|
338
381
|
/* Tear the outgoing page + divergent layouts down BEFORE publishing the
|
|
339
382
|
new snapshot. Publishing first would re-run the doomed leaf page's
|
|
@@ -341,13 +384,10 @@ export function router(
|
|
|
341
384
|
`undefined`, e.g. `Number(page.params.id)` → NaN → a bogus request)
|
|
342
385
|
while it's still mounted. Disposing first kills that scope; surviving
|
|
343
386
|
prefix layouts then update in place on publish. */
|
|
344
|
-
|
|
387
|
+
disposeFrom(divergence)
|
|
345
388
|
const url = resolveUrl(path)
|
|
346
389
|
clientPage.value = { route: chainRoute, params, url, navigating: false }
|
|
347
|
-
|
|
348
|
-
base.textContent = ''
|
|
349
|
-
}
|
|
350
|
-
buildFrom(base, divergence, chainKeys, layoutViews, pageView, params, hydrating)
|
|
390
|
+
buildFrom(divergence, chainKeys, layoutViews, pageView, key, params, hydrating)
|
|
351
391
|
/* Reapply the destination entry's scroll once its DOM exists — a
|
|
352
392
|
back/forward restores its offset, a fresh nav scrolls to the `#hash`
|
|
353
393
|
anchor (now built) or the top. Runs on the initial paint too: with
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* The comment-marker boundary a layout's `<slot/>` outlet lowers to — replacing the old
|
|
2
|
+
`<abide-outlet>` ELEMENT, so the next chain layer the router fills in lays out as a true
|
|
3
|
+
direct child of the slot's parent (no wrapper box breaking the layout's flex/grid/`:first-child`).
|
|
4
|
+
|
|
5
|
+
`abide:outlet` / `/abide:outlet` deliberately match `skeleton`'s `isOpenMarker`/`isCloseMarker`
|
|
6
|
+
(the `abide:` / `/abide:` convention), so a layout's own hole-scanning treats the outlet's
|
|
7
|
+
future child content as a balanced range and skips it — exactly like an `await`/`try` boundary.
|
|
8
|
+
The router fills the boundary with the next layer (see `outlet`/`fillBoundary`). */
|
|
9
|
+
export const OUTLET_OPEN = 'abide:outlet'
|
|
10
|
+
export const OUTLET_CLOSE = '/abide:outlet'
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/* The
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
they all agree on the marker. */
|
|
1
|
+
/* The compile-time AST tag a layout's `<slot/>` is rewritten to (`asOutlet`). It is a
|
|
2
|
+
sentinel only — never a rendered element: both compiler back-ends lower it to an empty
|
|
3
|
+
`<!--abide:outlet-->`…`<!--/abide:outlet-->` comment boundary (`outlet` on the client,
|
|
4
|
+
the marker string in SSR) the router fills with the next chain layer. Shared by both
|
|
5
|
+
back-ends and `skeletonContext` so they all agree which node is the outlet. */
|
|
7
6
|
// @documentation plumbing
|
|
8
7
|
export const OUTLET_TAG = 'abide-outlet'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* The outlet boundary the most recent `outlet()` call established (a layout's `<slot/>`
|
|
2
|
+
fill point, or the router's root boundary in `#app`). The router reads it right after
|
|
3
|
+
building/claiming a layer — synchronously, so it is exactly that layer's single slot —
|
|
4
|
+
to learn where the NEXT chain layer mounts, without scanning the DOM. A layer with no
|
|
5
|
+
slot (the leaf page) leaves it whatever the router reset it to. */
|
|
6
|
+
export const PENDING_OUTLET: { current: { open: Comment; close: Comment } | undefined } = {
|
|
7
|
+
current: undefined,
|
|
8
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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`. */
|
|
@@ -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,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
|
-
}
|