@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.
Files changed (39) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/package.json +2 -1
  4. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +4 -3
  5. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
  6. package/src/lib/ui/compile/asOutlet.ts +6 -5
  7. package/src/lib/ui/compile/compileModule.ts +13 -9
  8. package/src/lib/ui/compile/generateBuild.ts +55 -44
  9. package/src/lib/ui/compile/generateSSR.ts +30 -21
  10. package/src/lib/ui/compile/isAnchorPositioned.ts +19 -0
  11. package/src/lib/ui/compile/isControlFlow.ts +3 -1
  12. package/src/lib/ui/compile/isTextLeaf.ts +1 -1
  13. package/src/lib/ui/compile/skeletonContext.ts +19 -20
  14. package/src/lib/ui/compile/skeletonable.ts +3 -2
  15. package/src/lib/ui/dom/commentData.ts +14 -0
  16. package/src/lib/ui/dom/disposeRange.ts +21 -0
  17. package/src/lib/ui/dom/fillBoundary.ts +38 -0
  18. package/src/lib/ui/dom/fillRange.ts +30 -0
  19. package/src/lib/ui/dom/hydrate.ts +11 -21
  20. package/src/lib/ui/dom/mount.ts +16 -25
  21. package/src/lib/ui/dom/mountChild.ts +27 -14
  22. package/src/lib/ui/dom/mountRange.ts +45 -0
  23. package/src/lib/ui/dom/outlet.ts +62 -0
  24. package/src/lib/ui/dom/scopeLabel.ts +8 -6
  25. package/src/lib/ui/dom/skeleton.ts +11 -30
  26. package/src/lib/ui/dom/withScope.ts +33 -0
  27. package/src/lib/ui/installHotBridge.ts +2 -0
  28. package/src/lib/ui/renderChain.ts +23 -15
  29. package/src/lib/ui/router.ts +78 -38
  30. package/src/lib/ui/runtime/OUTLET_MARKER.ts +10 -0
  31. package/src/lib/ui/runtime/OUTLET_TAG.ts +5 -6
  32. package/src/lib/ui/runtime/PENDING_OUTLET.ts +8 -0
  33. package/src/lib/ui/runtime/captureModelDoc.ts +12 -13
  34. package/src/lib/ui/runtime/hotReplace.ts +14 -10
  35. package/src/lib/ui/runtime/types/HotInstance.ts +11 -7
  36. package/src/lib/ui/runtime/types/Route.ts +6 -5
  37. package/src/lib/ui/runtime/types/UiComponent.ts +5 -0
  38. package/src/lib/ui/compile/componentWrapperTag.ts +0 -15
  39. package/src/lib/ui/runtime/firstOutlet.ts +0 -22
@@ -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
- /* A layout mounted in the active chain: its route key (the directory URL — its
18
- identity for the diff), the disposer that stops its reactivity, and the outlet
19
- element the next layer mounts into. */
20
- type MountedLayout = { key: string; dispose: () => void; outlet: Element }
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 mount that returns no disposer still needs one for the chain teardown. */
23
- const noop = (): void => {}
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), then
94
- return the container the rebuild mounts into the surviving layout's outlet, or
95
- `host` when the whole chain goes. */
96
- const disposeFrom = (index: number): Element => {
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
- /* Mount (or hydrate) the chain tail — layouts `[index..]` then the page into
108
- `base`, threading each layer into the previous one's outlet. Hydration brackets
109
- ONE render pass across all layers so await/try block ids stay unique and aligned
110
- with the SSR stream; a fresh mount needs no shared pass. */
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
- let container = base
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
- const dispose = hydrating
125
- ? (view.hydrate as NonNullable<Route['hydrate']>)(container, params)
126
- : (view(container, params) ?? noop)
127
- const outlet = firstOutlet(container)
128
- if (outlet === undefined) {
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, outlet })
132
- container = outlet
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 = hydrating
138
- ? (pageView.hydrate as NonNullable<Route['hydrate']>)(container, params)
139
- : (pageView(container, params) ?? undefined)
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
- clear its DOM (a fresh mount; hydration adopts in place), rebuild. */
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
- const base = disposeFrom(divergence)
387
+ disposeFrom(divergence)
345
388
  const url = resolveUrl(path)
346
389
  clientPage.value = { route: chainRoute, params, url, navigating: false }
347
- if (!hydrating) {
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 element a layout's `<slot/>` lowers to: an empty structural container the
2
- router fills with the next layer of the route's layout chain (the nested layout
3
- or the page). SSR emits it empty so the renderer can fold the child's html into
4
- it; on the client the router mounts/hydrates the child into it and finds it by
5
- tag. Shared by both compiler back-ends, the SSR chain composer, and the router so
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 disposer alongside the component's own
6
- `model` document — the serializable `state` doc, needed so a hot swap can carry
7
- its value across (see `hotReplace`). The model is found, not threaded: a component
8
- seeds its `model` first (the desugared `const model = doc({})` + its init patches
9
- run before any child mounts or control-flow blocks), so the FIRST patch announced
10
- on the bus during the build names it. A component with no `state` mints no model
11
- and emits nothing first `model` is then `undefined` and there is nothing to
12
- preserve. Used only on the hot path; the subscription is torn down with the build.
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: () => () => void): {
15
- dispose: () => void
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
- dispose = build()
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 mount disposer clears
9
- the host), re-run `next` into the same host with the same props, then re-seed the
10
- fresh model from the snapshot so the user's in-progress `state` survives the edit
11
- (state above the boundary already survives: props are thunks re-reading the parent's
12
- live signals). The hot module calls this on load with its freshly compiled `next`.
13
- Returns whether it swapped at least one instance: false (the edited component has
14
- none mounted — e.g. a router-mounted page, or a hidden branch) tells the caller to
15
- fall back to a full reload, since nothing on screen would update.
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 { dispose, model } = captureModelDoc(() => next(instance.host, instance.props))
27
- instance.dispose = dispose
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: its wrapper host, the
6
- factory that built it, the props (thunks re-read on a swap so the parent's live
7
- state still flows through), the disposer for its current scope + DOM, and its own
8
- `model` document (`undefined` when the component has no `state`) snapshotted
9
- before a swap and re-seeded after, so the user's in-progress state survives the
10
- edit. A swap mutates `factory`/`dispose`/`model` in place (see `hotReplace`).
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
- host: Element
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: mounts into a host (optionally with props) and may return a
2
- disposer the router calls when navigating away. A compiled `.abide` default
3
- export also carries `hydrate` (adopt SSR in place) and `hydratable` (false when
4
- the page has an `await` block), which the router uses for the initial render. */
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
- hydrate?: (host: Element, props?: unknown) => () => void
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
- }