@uniflowed/router 0.0.0-alpha.34 → 0.0.0-alpha.37
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/client.js +24 -40
- package/index.js +1 -0
- package/internal/base-path.js +175 -0
- package/internal/boundaries.js +48 -25
- package/internal/compose.js +478 -0
- package/internal/error-view.js +189 -0
- package/internal/flight-browser.js +228 -0
- package/internal/flight-chunks.js +181 -0
- package/internal/flight-ssr.js +78 -0
- package/internal/flight.js +158 -0
- package/internal/head.js +219 -0
- package/internal/prepare-document.js +49 -0
- package/internal/react-version.js +77 -0
- package/internal/resolve.js +1615 -0
- package/internal/runtime.js +605 -2465
- package/internal/server-route.js +58 -0
- package/internal/shell.js +107 -0
- package/internal/stream.js +216 -3
- package/middleware.js +144 -14
- package/package.json +25 -6
- package/rsc-client.js +117 -0
- package/rsc-ssr.js +432 -0
- package/rsc.js +329 -0
- package/server-components.js +159 -0
- package/server.js +42 -88
package/client.js
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
// *before* `hydrateRoot`, so the first client render is synchronous and
|
|
15
15
|
// matches the server's markup exactly.
|
|
16
16
|
//
|
|
17
|
+
// An application whose routes render as React Server Components, the default,
|
|
18
|
+
// starts from neither: `hydrateFlight` in `./rsc-client.js` hydrates the Flight
|
|
19
|
+
// payload its document carries. That is a separate entry so that this one,
|
|
20
|
+
// which every other web application imports, never names React's Flight
|
|
21
|
+
// client. The client is an optional peer that needs React 19.3, and a project
|
|
22
|
+
// on React 19.2 does not install it (ubugeeei-prod/uf#992).
|
|
23
|
+
//
|
|
17
24
|
// # What "its loader data" means once the loader can defer
|
|
18
25
|
//
|
|
19
26
|
// It is a payload rather than a value: `internal/payload.js` writes the model
|
|
@@ -101,6 +108,7 @@ import {
|
|
|
101
108
|
hasClientPage,
|
|
102
109
|
installNavigation,
|
|
103
110
|
installRoutes,
|
|
111
|
+
installRouting,
|
|
104
112
|
matchRoute,
|
|
105
113
|
resolveFailure,
|
|
106
114
|
resolveMatch,
|
|
@@ -108,6 +116,8 @@ import {
|
|
|
108
116
|
import { DATA_ID, ROOT_ID } from "./internal/document.js";
|
|
109
117
|
import { decodePayload } from "./internal/payload.js";
|
|
110
118
|
import { createPayloadReader, domObserver } from "./internal/payload-rows.js";
|
|
119
|
+
import { prepareDocumentForHydration } from "./internal/prepare-document.js";
|
|
120
|
+
import { type TrailingSlash, addressOf, applicationPathOf } from "./internal/base-path.js";
|
|
111
121
|
|
|
112
122
|
/**
|
|
113
123
|
* Hydrate the current document.
|
|
@@ -123,6 +133,8 @@ export async function hydrate(options: {|
|
|
|
123
133
|
readonly errors: RouteTable["errors"],
|
|
124
134
|
readonly strictMode?: boolean,
|
|
125
135
|
readonly navigation?: Navigation,
|
|
136
|
+
readonly basePath?: string,
|
|
137
|
+
readonly trailingSlash?: TrailingSlash,
|
|
126
138
|
|}): Promise<void> {
|
|
127
139
|
const table: RouteTable = {
|
|
128
140
|
routes: options.routes,
|
|
@@ -135,15 +147,18 @@ export async function hydrate(options: {|
|
|
|
135
147
|
// `app.rendering.navigation` existed and what a hand-written entry still
|
|
136
148
|
// means: the default is the behaviour, not the absence of one.
|
|
137
149
|
installNavigation(options.navigation ?? "client");
|
|
150
|
+
installRouting({ basePath: options.basePath, trailingSlash: options.trailingSlash });
|
|
151
|
+
// The route table has no base path in it, and the address bar does.
|
|
152
|
+
const applicationPath = applicationPathOf(window.location.pathname) ?? window.location.pathname;
|
|
138
153
|
|
|
139
154
|
// Before the loader data is read and before `resolveMatch` is called: both
|
|
140
155
|
// would go looking for a page module that is not in this bundle.
|
|
141
|
-
const matched = matchRoute(table.routes,
|
|
156
|
+
const matched = matchRoute(table.routes, applicationPath);
|
|
142
157
|
if (matched != null && !hasClientPage(matched.route)) {
|
|
143
158
|
return;
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
const url =
|
|
161
|
+
const url = applicationPath + window.location.search;
|
|
147
162
|
// Row 0 of the payload, and the reader that will fill in the rows it refers
|
|
148
163
|
// to. Both before `hydrateRoot`, and in this order: `decodePayload` is what
|
|
149
164
|
// tells the reader which rows the page is waiting for, and `watch` is what
|
|
@@ -217,42 +232,6 @@ export async function hydrate(options: {|
|
|
|
217
232
|
}
|
|
218
233
|
}
|
|
219
234
|
|
|
220
|
-
function prepareDocumentForHydration(document: Document): void {
|
|
221
|
-
const head = document.head;
|
|
222
|
-
const envelope = head.querySelector('meta[name="uf:render"]');
|
|
223
|
-
if (envelope != null && head.firstChild !== envelope) {
|
|
224
|
-
head.insertBefore(envelope, head.firstChild);
|
|
225
|
-
}
|
|
226
|
-
moveLayoutMetaAfterRouteHead(head, head.querySelector("meta[charset]"));
|
|
227
|
-
moveLayoutMetaAfterRouteHead(head, head.querySelector('meta[name="viewport"]'));
|
|
228
|
-
document.getElementById("_R_")?.remove();
|
|
229
|
-
normalizeReactFormActions(document);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function moveLayoutMetaAfterRouteHead(head: HTMLHeadElement, meta: Element | null): void {
|
|
233
|
-
if (meta == null) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const colorScheme = head.querySelector('meta[name="color-scheme"]');
|
|
237
|
-
if (colorScheme != null && colorScheme !== meta) {
|
|
238
|
-
head.insertBefore(meta, colorScheme);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
head.appendChild(meta);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const SERVER_FORM_PLACEHOLDER = "javascript:throw new Error('React form unexpectedly submitted.')";
|
|
245
|
-
const CLIENT_FORM_PLACEHOLDER =
|
|
246
|
-
"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')";
|
|
247
|
-
|
|
248
|
-
function normalizeReactFormActions(document: Document): void {
|
|
249
|
-
for (const form of document.querySelectorAll("form")) {
|
|
250
|
-
if (form.getAttribute("action") === SERVER_FORM_PLACEHOLDER) {
|
|
251
|
-
form.setAttribute("action", CLIENT_FORM_PLACEHOLDER);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
235
|
/**
|
|
257
236
|
* Render the current route into an empty shell.
|
|
258
237
|
*
|
|
@@ -289,6 +268,8 @@ export async function render(options: {|
|
|
|
289
268
|
readonly errors: RouteTable["errors"],
|
|
290
269
|
readonly strictMode?: boolean,
|
|
291
270
|
readonly navigation?: Navigation,
|
|
271
|
+
readonly basePath?: string,
|
|
272
|
+
readonly trailingSlash?: TrailingSlash,
|
|
292
273
|
|}): Promise<void> {
|
|
293
274
|
const table: RouteTable = {
|
|
294
275
|
routes: options.routes,
|
|
@@ -297,14 +278,17 @@ export async function render(options: {|
|
|
|
297
278
|
};
|
|
298
279
|
installRoutes(table);
|
|
299
280
|
installNavigation(options.navigation ?? "client");
|
|
281
|
+
installRouting({ basePath: options.basePath, trailingSlash: options.trailingSlash });
|
|
300
282
|
|
|
301
|
-
const url =
|
|
283
|
+
const url =
|
|
284
|
+
(applicationPathOf(window.location.pathname) ?? window.location.pathname) +
|
|
285
|
+
window.location.search;
|
|
302
286
|
let resolved;
|
|
303
287
|
try {
|
|
304
288
|
resolved = await resolveMatch(table, url);
|
|
305
289
|
} catch (error) {
|
|
306
290
|
if (error instanceof RedirectError) {
|
|
307
|
-
window.location.replace(error.to);
|
|
291
|
+
window.location.replace(addressOf(error.to));
|
|
308
292
|
return;
|
|
309
293
|
}
|
|
310
294
|
// The error boundary, chosen the same way the server chooses it. A throw
|
package/index.js
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: `app.router.basePath` and
|
|
4
|
+
// `app.router.trailingSlash`, as the router reads them.
|
|
5
|
+
//
|
|
6
|
+
// The route table has no base in it — `/guide` is `/guide` whether the site is
|
|
7
|
+
// served at `/` or at `/docs` — and the browser's address bar has one. This
|
|
8
|
+
// module is the one place those two spellings meet: a `Link` and a navigation
|
|
9
|
+
// turn an application path into the address, and hydration, `popstate` and a
|
|
10
|
+
// refresh turn the address back into an application path before the table is
|
|
11
|
+
// asked. Every other part of the router speaks application paths.
|
|
12
|
+
//
|
|
13
|
+
// The policy decides the spelling of a path a link writes, so that a page is
|
|
14
|
+
// linked by the address its server answers without a redirect. The server's
|
|
15
|
+
// half — the `308` for the other spelling, and the `404` outside the base — is
|
|
16
|
+
// `@uniflowed/server`'s `internal/routing.js`.
|
|
17
|
+
//
|
|
18
|
+
// # Installed, like navigation
|
|
19
|
+
//
|
|
20
|
+
// Module state beside `installNavigation`, set once by the entry that started
|
|
21
|
+
// the application: `virtual:uf/client` in the browser and `virtual:uf/server`
|
|
22
|
+
// in the graph that renders documents. `@uniflowed/vite` writes both calls from
|
|
23
|
+
// `uf.config.js`, so a dev server and a build link the same way. A test that
|
|
24
|
+
// installs nothing gets the root and `"ignore"`, which is what every
|
|
25
|
+
// application had before the setting existed.
|
|
26
|
+
|
|
27
|
+
/** Which spelling of a path is the page; see `app.router.trailingSlash`. */
|
|
28
|
+
export type TrailingSlash = "never" | "always" | "ignore";
|
|
29
|
+
|
|
30
|
+
/** What an entry installs. */
|
|
31
|
+
export type RoutingSettings = {|
|
|
32
|
+
readonly basePath?: string,
|
|
33
|
+
readonly trailingSlash?: TrailingSlash,
|
|
34
|
+
|};
|
|
35
|
+
|
|
36
|
+
let installedBase: string = "";
|
|
37
|
+
let installedSlash: TrailingSlash = "ignore";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Say where this application is served and how its paths are spelled. Called
|
|
41
|
+
* once, by the entry that starts it.
|
|
42
|
+
*/
|
|
43
|
+
export function installRouting(settings: RoutingSettings): void {
|
|
44
|
+
installedBase = normalizeBase(settings.basePath ?? "");
|
|
45
|
+
installedSlash = settings.trailingSlash ?? "ignore";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The path this application is served under: `""` at the root, `"/docs"`
|
|
50
|
+
* otherwise.
|
|
51
|
+
*
|
|
52
|
+
* For code that builds an absolute address the router did not build for it —
|
|
53
|
+
* a middleware's `Response.redirect(new URL(`${basePath()}/sign-in`, request.url))`
|
|
54
|
+
* — because a route handler and a middleware are handed the application path,
|
|
55
|
+
* and an address built from `/sign-in` alone leaves the base behind.
|
|
56
|
+
*/
|
|
57
|
+
export function basePath(): string {
|
|
58
|
+
return installedBase;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** How this application spells a path. */
|
|
62
|
+
export function trailingSlash(): TrailingSlash {
|
|
63
|
+
return installedSlash;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The address for an application path: the base in front, the policy's
|
|
68
|
+
* spelling, the query and the fragment kept.
|
|
69
|
+
*
|
|
70
|
+
* Only a path that starts with a single `/` is an application path. Anything
|
|
71
|
+
* else — `https://…`, `//cdn…`, `mailto:`, `?page=2`, `#top`, `../up` — is
|
|
72
|
+
* returned as it was written, because the browser resolves it against the
|
|
73
|
+
* address that is already there.
|
|
74
|
+
*/
|
|
75
|
+
export function addressOf(to: string): string {
|
|
76
|
+
if (!to.startsWith("/") || to.startsWith("//")) {
|
|
77
|
+
return to;
|
|
78
|
+
}
|
|
79
|
+
const { path, rest } = splitPath(to);
|
|
80
|
+
return `${installedBase}${spellPath(path, installedSlash, installedBase !== "")}${rest}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The application path for an address's pathname, or `null` when the address
|
|
85
|
+
* is outside the base.
|
|
86
|
+
*
|
|
87
|
+
* `/docs/guide` is `/guide` under `/docs`, and `/docs` and `/docs/` are both
|
|
88
|
+
* `/`. `/docsx` is not under `/docs`: a base is whole segments.
|
|
89
|
+
*/
|
|
90
|
+
export function applicationPathOf(pathname: string): string | null {
|
|
91
|
+
if (installedBase === "") {
|
|
92
|
+
return pathname;
|
|
93
|
+
}
|
|
94
|
+
if (pathname === installedBase) {
|
|
95
|
+
return "/";
|
|
96
|
+
}
|
|
97
|
+
if (pathname.startsWith(`${installedBase}/`)) {
|
|
98
|
+
return pathname.slice(installedBase.length);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A browser pathname in this application's spelling: the base kept, the
|
|
105
|
+
* application path under it spelled by the policy. A pathname outside the
|
|
106
|
+
* base is returned as it was.
|
|
107
|
+
*
|
|
108
|
+
* For an address the router read back rather than wrote — the document path a
|
|
109
|
+
* payload URL names has lost its trailing slash, and the history entry a
|
|
110
|
+
* navigation writes should be the address the server answers without a
|
|
111
|
+
* redirect.
|
|
112
|
+
*/
|
|
113
|
+
export function canonicalAddress(pathname: string): string {
|
|
114
|
+
const application = applicationPathOf(pathname);
|
|
115
|
+
if (application == null) {
|
|
116
|
+
return pathname;
|
|
117
|
+
}
|
|
118
|
+
const spelled = spellPath(application, installedSlash, installedBase !== "");
|
|
119
|
+
return `${installedBase}${spelled}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `path` in `policy`'s spelling.
|
|
124
|
+
*
|
|
125
|
+
* The root of an application at the root is `/` whatever the policy says. The
|
|
126
|
+
* root of an application under a base is the base itself — `/docs` — unless
|
|
127
|
+
* the policy is `"always"`, which spells it `/docs/`. A path whose last segment
|
|
128
|
+
* looks like a file keeps what it was written with, because `/robots.txt/` is
|
|
129
|
+
* not a page.
|
|
130
|
+
*/
|
|
131
|
+
export function spellPath(path: string, policy: TrailingSlash, underBase: boolean): string {
|
|
132
|
+
let end = path.length;
|
|
133
|
+
while (end > 0 && path.charCodeAt(end - 1) === 47) {
|
|
134
|
+
end -= 1;
|
|
135
|
+
}
|
|
136
|
+
const trimmed = path.slice(0, end);
|
|
137
|
+
if (trimmed === "") {
|
|
138
|
+
return policy === "always" || !underBase ? "/" : "";
|
|
139
|
+
}
|
|
140
|
+
if (policy === "ignore" || looksLikeAFile(trimmed)) {
|
|
141
|
+
return path;
|
|
142
|
+
}
|
|
143
|
+
return policy === "always" ? `${trimmed}/` : trimmed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether a path's last segment has an extension. */
|
|
147
|
+
function looksLikeAFile(path: string): boolean {
|
|
148
|
+
const last = path.slice(path.lastIndexOf("/") + 1);
|
|
149
|
+
return last.includes(".");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function splitPath(to: string): {| readonly path: string, readonly rest: string |} {
|
|
153
|
+
let end = to.length;
|
|
154
|
+
for (let index = 0; index < to.length; index += 1) {
|
|
155
|
+
const code = to.charCodeAt(index);
|
|
156
|
+
// `?` and `#`.
|
|
157
|
+
if (code === 63 || code === 35) {
|
|
158
|
+
end = index;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { path: to.slice(0, end), rest: to.slice(end) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A base as `uf_config` accepts one, with a trailing slash forgiven: `""`,
|
|
167
|
+
* `"/"` and absent are the root.
|
|
168
|
+
*/
|
|
169
|
+
function normalizeBase(base: string): string {
|
|
170
|
+
let end = base.length;
|
|
171
|
+
while (end > 0 && base.charCodeAt(end - 1) === 47) {
|
|
172
|
+
end -= 1;
|
|
173
|
+
}
|
|
174
|
+
return base.slice(0, end);
|
|
175
|
+
}
|
package/internal/boundaries.js
CHANGED
|
@@ -83,8 +83,10 @@
|
|
|
83
83
|
// package is `sideEffects: false`, so with the references folded away the
|
|
84
84
|
// module is dropped rather than merely unused.
|
|
85
85
|
|
|
86
|
+
"use client";
|
|
87
|
+
|
|
86
88
|
import * as React from "react";
|
|
87
|
-
import { useEffect,
|
|
89
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
88
90
|
|
|
89
91
|
import { SYNTHESISED_SOURCE } from "./boundary-data.js";
|
|
90
92
|
import { reportDiagnostic } from "./diagnostics.js";
|
|
@@ -114,13 +116,32 @@ export const BOUNDARY_GLOBAL: string = "__ufBoundaries";
|
|
|
114
116
|
/**
|
|
115
117
|
* Whether an edge that mounts now should be in the DOM immediately.
|
|
116
118
|
*
|
|
117
|
-
* Latched by the first edge to mount and never cleared
|
|
118
|
-
* `
|
|
119
|
-
* difference between "
|
|
119
|
+
* Latched by the first edge to mount and never cleared, and read through
|
|
120
|
+
* `useSyncExternalStore` rather than during the render body, which is the
|
|
121
|
+
* difference between "a value React asked for" and "a module variable a
|
|
120
122
|
* memoising compiler is entitled to hold on to".
|
|
121
123
|
*/
|
|
122
124
|
let marksAreLive = false;
|
|
123
125
|
|
|
126
|
+
/** The edges waiting to hear that marks have gone live. */
|
|
127
|
+
const liveListeners: Set<() => void> = new Set();
|
|
128
|
+
|
|
129
|
+
function subscribeToLiveMarks(listener: () => void): () => void {
|
|
130
|
+
liveListeners.add(listener);
|
|
131
|
+
return () => {
|
|
132
|
+
liveListeners.delete(listener);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function marksAreLiveNow(): boolean {
|
|
137
|
+
return marksAreLive;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** What a server rendered, and so what every hydrating edge renders: nothing. */
|
|
141
|
+
function noMarksOnTheServer(): boolean {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
/**
|
|
125
146
|
* One end of one boundary.
|
|
126
147
|
*
|
|
@@ -128,12 +149,25 @@ let marksAreLive = false;
|
|
|
128
149
|
* This used to be a `<template>`, but React 19.3 reports template insertion
|
|
129
150
|
* during document-root hydration as a browser error. A `span hidden` carries
|
|
130
151
|
* the same marker data without entering layout or the accessibility tree.
|
|
152
|
+
*
|
|
153
|
+
* # Why the server snapshot, and not a first state
|
|
154
|
+
*
|
|
155
|
+
* An edge used to take `marksAreLive` as its first state, which is right only
|
|
156
|
+
* if every edge on a page hydrates in the same pass. Under React Server
|
|
157
|
+
* Components they do not: a client reference loads when the payload names it,
|
|
158
|
+
* so the part of the tree above it hydrates, commits and runs this effect
|
|
159
|
+
* first, and an edge that hydrates afterwards read `true` and rendered a mark
|
|
160
|
+
* the server never wrote — a hydration mismatch on every page with a boundary
|
|
161
|
+
* below a client component, under `uf dev` only. `useSyncExternalStore` hands a
|
|
162
|
+
* hydrating edge the server's answer whenever it hydrates, and an edge mounted
|
|
163
|
+
* by a navigation or by HMR the live one, in its own commit, as before.
|
|
131
164
|
*/
|
|
132
|
-
component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
|
|
133
|
-
const
|
|
165
|
+
export component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
|
|
166
|
+
const live = useSyncExternalStore(subscribeToLiveMarks, marksAreLiveNow, noMarksOnTheServer);
|
|
134
167
|
useEffect(() => {
|
|
168
|
+
if (marksAreLive) return;
|
|
135
169
|
marksAreLive = true;
|
|
136
|
-
|
|
170
|
+
for (const listener of [...liveListeners]) listener();
|
|
137
171
|
}, []);
|
|
138
172
|
if (!live) {
|
|
139
173
|
return null;
|
|
@@ -154,25 +188,14 @@ component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
|
|
|
154
188
|
/**
|
|
155
189
|
* `children`, between the two marks of `boundary`.
|
|
156
190
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
191
|
+
* Kept importable from here, where the marks are, and written in
|
|
192
|
+
* `./compose.js`, where they are placed. This module is a client module — an
|
|
193
|
+
* edge has state and an effect — and a server composing a tree for React Server
|
|
194
|
+
* Components calls the factory rather than rendering it, so the factory has to
|
|
195
|
+
* live in a module that graph evaluates while the edges it places stay
|
|
196
|
+
* references to this one. See ubugeeei-prod/uf#519.
|
|
163
197
|
*/
|
|
164
|
-
export
|
|
165
|
-
if (boundary == null) {
|
|
166
|
-
return children;
|
|
167
|
-
}
|
|
168
|
-
return (
|
|
169
|
-
<>
|
|
170
|
-
<BoundaryEdge boundary={boundary} edge="open" />
|
|
171
|
-
{children}
|
|
172
|
-
<BoundaryEdge boundary={boundary} edge="close" />
|
|
173
|
-
</>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
198
|
+
export { insideBoundary } from "./compose.js";
|
|
176
199
|
|
|
177
200
|
/**
|
|
178
201
|
* How far a walk between two marks will go before giving up.
|