@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11
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/README.md +499 -313
- package/THIRD_PARTY_NOTICES.md +98 -23
- package/dist/Text-DV9CuzAT.d.ts +452 -0
- package/dist/ansi.d.ts +217 -0
- package/dist/ansi.js +87 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BAC9-TZX.js +592 -0
- package/dist/color-policy-CCxuHIdD.d.ts +22 -0
- package/dist/color-profile-CyeHnG1T.d.ts +97 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-B3dL4Q11.js +374 -0
- package/dist/detect-Db6GbKOm.d.ts +195 -0
- package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-D48vQhhe.d.ts +21 -0
- package/dist/index-DDVME65c.d.ts +919 -0
- package/dist/index.d.ts +1310 -0
- package/dist/index.js +3211 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +227 -0
- package/dist/jsx-runtime-XtwIVFsg.js +255 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +8 -0
- package/dist/osc-BFKKSqpg.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-Cx-zC_sX.d.ts +81 -0
- package/dist/query-BNc2B8GD.d.ts +152 -0
- package/dist/react-CTZ_8dwh.js +1197 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +49 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +711 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-CgC2WlVM.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-DDQ5V300.js +723 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-C1P5fOUi.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +121 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-B5npZrJU.js +18736 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +89 -29
- package/src/ansi/chalk.ts +138 -0
- package/src/ansi/cursor.ts +46 -0
- package/src/ansi/east-asian-width.ts +259 -0
- package/src/ansi/escapes.ts +120 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +27 -0
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +234 -0
- package/src/ansi/slice.ts +43 -0
- package/src/ansi/string-width.ts +121 -0
- package/src/ansi/strip.ts +13 -0
- package/src/ansi/tokenize.ts +380 -0
- package/src/ansi/truncate.ts +196 -0
- package/src/ansi/wrap.ts +765 -0
- package/src/ansi-tokenizer.ts +510 -0
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +608 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +679 -0
- package/src/capabilities/store.ts +394 -0
- package/src/code-excerpt.ts +39 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AccessibilityContext.ts +5 -0
- package/src/components/AnimationContext.ts +24 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +878 -0
- package/src/components/AppContext.ts +111 -0
- package/src/components/BackgroundContext.ts +7 -0
- package/src/components/Box.tsx +100 -0
- package/src/components/CursorContext.ts +19 -0
- package/src/components/ErrorBoundary.tsx +39 -0
- package/src/components/ErrorOverview.tsx +135 -0
- package/src/components/FocusContext.ts +30 -0
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Newline.tsx +16 -0
- package/src/components/Spacer.tsx +11 -0
- package/src/components/Static.tsx +60 -0
- package/src/components/StderrContext.ts +24 -0
- package/src/components/StdinContext.ts +48 -0
- package/src/components/StdoutContext.ts +26 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +122 -0
- package/src/components/Transform.tsx +38 -0
- package/src/components/VirtualList.tsx +128 -0
- package/src/cursor-position.ts +103 -0
- package/src/devtools.ts +103 -0
- package/src/dom.ts +301 -0
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +11 -0
- package/src/global.d.ts +38 -0
- package/src/glyphs.ts +99 -0
- package/src/hooks/use-animation.ts +142 -0
- package/src/hooks/use-app.ts +8 -0
- package/src/hooks/use-box-metrics.ts +134 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +33 -0
- package/src/hooks/use-focus-manager.ts +62 -0
- package/src/hooks/use-focus.ts +82 -0
- package/src/hooks/use-input.ts +267 -0
- package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
- package/src/hooks/use-paste.ts +78 -0
- package/src/hooks/use-stderr.ts +8 -0
- package/src/hooks/use-stdin.ts +10 -0
- package/src/hooks/use-stdout.ts +8 -0
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-virtual-scroll.ts +84 -0
- package/src/hooks/use-window-size.ts +37 -0
- package/src/index.ts +96 -0
- package/src/ink.tsx +1452 -0
- package/src/input-parser.ts +303 -0
- package/src/instances.ts +9 -0
- package/src/jsx-dev-runtime.ts +3 -0
- package/src/jsx-runtime.ts +6 -0
- package/src/kitty-keyboard.ts +185 -0
- package/src/measure-element.ts +62 -0
- package/src/measure-text.ts +31 -0
- package/src/paint-tree.ts +220 -0
- package/src/parse-keypress.ts +515 -0
- package/src/parse-stack-line.ts +138 -0
- package/src/patch-console.ts +106 -0
- package/src/quick-lru.ts +85 -0
- package/src/react.ts +53 -0
- package/src/reconciler.ts +476 -0
- package/src/render-background.ts +59 -0
- package/src/render-border.ts +167 -0
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +146 -0
- package/src/render.ts +284 -0
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +33 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +106 -0
- package/src/squash-text-nodes.ts +37 -0
- package/src/stream.ts +28 -0
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +753 -0
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +92 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +242 -0
- package/src/terminal/session.ts +408 -0
- package/src/terminal-size.ts +58 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +361 -0
- package/src/testing/vitest.ts +157 -0
- package/src/throttle.ts +73 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +10 -0
- package/src/virtual-scroll.ts +133 -0
- package/src/wrap-text.ts +54 -0
- package/src/yoga/config.ts +57 -0
- package/src/yoga/core/absoluteLayout.ts +626 -0
- package/src/yoga/core/baseline.ts +66 -0
- package/src/yoga/core/cache.ts +136 -0
- package/src/yoga/core/calculateLayout.ts +2926 -0
- package/src/yoga/core/config.ts +104 -0
- package/src/yoga/core/flexLine.ts +177 -0
- package/src/yoga/core/helpers.ts +293 -0
- package/src/yoga/core/layoutResults.ts +167 -0
- package/src/yoga/core/node.ts +611 -0
- package/src/yoga/core/numeric.ts +44 -0
- package/src/yoga/core/pixelGrid.ts +151 -0
- package/src/yoga/core/style.ts +887 -0
- package/src/yoga/core/types.ts +224 -0
- package/src/yoga/generated/YGEnums.ts +263 -0
- package/src/yoga/index.ts +19 -0
- package/src/yoga/node.ts +1140 -0
- package/dist/index.d.mts +0 -2379
- package/dist/index.mjs +0 -10072
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Ported from react-router's createMemoryHistory (lib/router/history.ts, MIT),
|
|
2
|
+
// without URL semantics: no hash, no href/URL creation, no encoding.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
The type of navigation that produced the current location:
|
|
6
|
+
|
|
7
|
+
- `"POP"` — moving through existing history entries (`navigate(-1)`, initial load).
|
|
8
|
+
- `"PUSH"` — a new entry was added to the stack.
|
|
9
|
+
- `"REPLACE"` — the current entry was overwritten.
|
|
10
|
+
*/
|
|
11
|
+
export type NavigationType = "POP" | "PUSH" | "REPLACE";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
The two parseable pieces of a route path. Sigil routes have no hash — a
|
|
15
|
+
terminal has no scroll anchors.
|
|
16
|
+
*/
|
|
17
|
+
export type Path = {
|
|
18
|
+
/**
|
|
19
|
+
The path of the screen, beginning with `/`.
|
|
20
|
+
*/
|
|
21
|
+
pathname: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
The query string, beginning with `?`, or an empty string.
|
|
25
|
+
*/
|
|
26
|
+
search: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
An entry in the navigation stack.
|
|
31
|
+
*/
|
|
32
|
+
export type Location<State = unknown> = Path & {
|
|
33
|
+
/**
|
|
34
|
+
Arbitrary state attached to this entry via `navigate(to, {state})`. Unlike the
|
|
35
|
+
browser, this is held in memory and never serialized — any value works.
|
|
36
|
+
*/
|
|
37
|
+
state: State;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
A unique key for this entry, stable across re-renders. Useful as a React
|
|
41
|
+
`key` to remount a screen when re-navigating to the same path.
|
|
42
|
+
*/
|
|
43
|
+
key: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
A destination to navigate to: either a path string (`"/users/123?tab=posts"`)
|
|
48
|
+
or a partial `Path` object.
|
|
49
|
+
*/
|
|
50
|
+
export type To = string | Partial<Path>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
Splits a path string into its pathname and search parts.
|
|
54
|
+
*/
|
|
55
|
+
export const parsePath = (path: string): Partial<Path> => {
|
|
56
|
+
const parsedPath: Partial<Path> = {};
|
|
57
|
+
|
|
58
|
+
if (path) {
|
|
59
|
+
const searchIndex = path.indexOf("?");
|
|
60
|
+
if (searchIndex >= 0) {
|
|
61
|
+
parsedPath.search = path.slice(searchIndex);
|
|
62
|
+
path = path.slice(0, searchIndex);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (path) {
|
|
66
|
+
parsedPath.pathname = path;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return parsedPath;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
Joins a `Path` back into a single string.
|
|
75
|
+
*/
|
|
76
|
+
export const createPath = ({ pathname = "/", search = "" }: Partial<Path>): string =>
|
|
77
|
+
pathname + (search && search !== "?" ? (search.startsWith("?") ? search : `?${search}`) : "");
|
|
78
|
+
|
|
79
|
+
type Update = {
|
|
80
|
+
action: NavigationType;
|
|
81
|
+
location: Location;
|
|
82
|
+
delta: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type Listener = (update: Update) => void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
The in-memory navigation stack backing `<MemoryRouter>`.
|
|
89
|
+
*/
|
|
90
|
+
export type MemoryHistory = {
|
|
91
|
+
readonly index: number;
|
|
92
|
+
readonly action: NavigationType;
|
|
93
|
+
readonly location: Location;
|
|
94
|
+
readonly canGoBack: boolean;
|
|
95
|
+
readonly canGoForward: boolean;
|
|
96
|
+
push: (to: To, state?: unknown) => void;
|
|
97
|
+
replace: (to: To, state?: unknown) => void;
|
|
98
|
+
go: (delta: number) => void;
|
|
99
|
+
listen: (listener: Listener) => () => void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
An entry to seed the navigation stack with: a path string or a partial
|
|
104
|
+
`Location` (which may carry `state`).
|
|
105
|
+
*/
|
|
106
|
+
export type InitialEntry = string | Partial<Location>;
|
|
107
|
+
|
|
108
|
+
type MemoryHistoryOptions = {
|
|
109
|
+
initialEntries?: InitialEntry[];
|
|
110
|
+
initialIndex?: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const createMemoryHistory = ({
|
|
114
|
+
initialEntries = ["/"],
|
|
115
|
+
initialIndex,
|
|
116
|
+
}: MemoryHistoryOptions = {}): MemoryHistory => {
|
|
117
|
+
let keyCounter = 0;
|
|
118
|
+
const createKey = () => `k${keyCounter++}`;
|
|
119
|
+
|
|
120
|
+
const createLocation = (to: To, state: unknown = null): Location => {
|
|
121
|
+
const path = typeof to === "string" ? parsePath(to) : to;
|
|
122
|
+
const location: Location = {
|
|
123
|
+
pathname: path.pathname ?? "/",
|
|
124
|
+
search: path.search ?? "",
|
|
125
|
+
state,
|
|
126
|
+
key: createKey(),
|
|
127
|
+
};
|
|
128
|
+
if (!location.pathname.startsWith("/")) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Route pathnames must be absolute, but "${location.pathname}" was used to initialize the navigation stack.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return location;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const entries: Location[] = initialEntries.map((entry) =>
|
|
138
|
+
createLocation(entry, typeof entry === "string" ? null : (entry.state ?? null)),
|
|
139
|
+
);
|
|
140
|
+
let index = Math.min(Math.max(initialIndex ?? entries.length - 1, 0), entries.length - 1);
|
|
141
|
+
let action: NavigationType = "POP";
|
|
142
|
+
let listener: Listener | null = null;
|
|
143
|
+
|
|
144
|
+
const history: MemoryHistory = {
|
|
145
|
+
get index() {
|
|
146
|
+
return index;
|
|
147
|
+
},
|
|
148
|
+
get action() {
|
|
149
|
+
return action;
|
|
150
|
+
},
|
|
151
|
+
get location() {
|
|
152
|
+
return entries[index]!;
|
|
153
|
+
},
|
|
154
|
+
get canGoBack() {
|
|
155
|
+
return index > 0;
|
|
156
|
+
},
|
|
157
|
+
get canGoForward() {
|
|
158
|
+
return index < entries.length - 1;
|
|
159
|
+
},
|
|
160
|
+
push(to, state) {
|
|
161
|
+
action = "PUSH";
|
|
162
|
+
const nextLocation = createLocation(to, state);
|
|
163
|
+
index += 1;
|
|
164
|
+
// Pushing truncates any forward entries, exactly like browser history.
|
|
165
|
+
entries.splice(index, entries.length, nextLocation);
|
|
166
|
+
listener?.({ action, location: nextLocation, delta: 1 });
|
|
167
|
+
},
|
|
168
|
+
replace(to, state) {
|
|
169
|
+
action = "REPLACE";
|
|
170
|
+
const nextLocation = createLocation(to, state);
|
|
171
|
+
entries[index] = nextLocation;
|
|
172
|
+
listener?.({ action, location: nextLocation, delta: 0 });
|
|
173
|
+
},
|
|
174
|
+
go(delta) {
|
|
175
|
+
action = "POP";
|
|
176
|
+
const nextIndex = Math.min(Math.max(index + delta, 0), entries.length - 1);
|
|
177
|
+
const actualDelta = nextIndex - index;
|
|
178
|
+
index = nextIndex;
|
|
179
|
+
listener?.({ action, location: entries[index]!, delta: actualDelta });
|
|
180
|
+
},
|
|
181
|
+
listen(newListener) {
|
|
182
|
+
if (listener) {
|
|
183
|
+
throw new Error("A memory history only supports one listener at a time.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
listener = newListener;
|
|
187
|
+
return () => {
|
|
188
|
+
listener = null;
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return history;
|
|
194
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
// Ported from react-router's declarative-mode hooks (lib/hooks.tsx, MIT),
|
|
3
|
+
// with the data-router branches removed.
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
LocationContext,
|
|
15
|
+
NavigationContext,
|
|
16
|
+
OutletContext,
|
|
17
|
+
RouteContext,
|
|
18
|
+
} from "#/router/context.ts";
|
|
19
|
+
import {
|
|
20
|
+
parsePath,
|
|
21
|
+
type Location,
|
|
22
|
+
type NavigationType,
|
|
23
|
+
type Path,
|
|
24
|
+
type To,
|
|
25
|
+
} from "#/router/history.ts";
|
|
26
|
+
import {
|
|
27
|
+
getResolveToMatches,
|
|
28
|
+
joinPaths,
|
|
29
|
+
matchPath,
|
|
30
|
+
matchRoutes,
|
|
31
|
+
resolveTo,
|
|
32
|
+
type Params,
|
|
33
|
+
type PathMatch,
|
|
34
|
+
type PathPattern,
|
|
35
|
+
type RouteMatch,
|
|
36
|
+
type RouteObject,
|
|
37
|
+
} from "#/router/matcher.ts";
|
|
38
|
+
|
|
39
|
+
const warned = new Set<string>();
|
|
40
|
+
const warnOnce = (key: string, message: string): void => {
|
|
41
|
+
if (!warned.has(key)) {
|
|
42
|
+
warned.add(key);
|
|
43
|
+
console.warn(message);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
Returns `true` when rendered inside a `<MemoryRouter>`. Useful for components
|
|
49
|
+
that optionally integrate with routing.
|
|
50
|
+
*/
|
|
51
|
+
export const useInRouterContext = (): boolean => useContext(LocationContext) != null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
Returns the current `Location`.
|
|
55
|
+
*/
|
|
56
|
+
export const useLocation = (): Location => {
|
|
57
|
+
const locationContext = useContext(LocationContext);
|
|
58
|
+
if (!locationContext) {
|
|
59
|
+
throw new Error("useLocation() may be used only in the context of a <MemoryRouter> component.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return locationContext.location;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
Returns the type of navigation that produced the current location: `"POP"`,
|
|
67
|
+
`"PUSH"`, or `"REPLACE"`.
|
|
68
|
+
*/
|
|
69
|
+
export const useNavigationType = (): NavigationType => {
|
|
70
|
+
const locationContext = useContext(LocationContext);
|
|
71
|
+
if (!locationContext) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"useNavigationType() may be used only in the context of a <MemoryRouter> component.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return locationContext.navigationType;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const useNavigationContext = (hookName: string) => {
|
|
81
|
+
const navigationContext = useContext(NavigationContext);
|
|
82
|
+
if (!navigationContext) {
|
|
83
|
+
throw new Error(`${hookName} may be used only in the context of a <MemoryRouter> component.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return navigationContext;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type NavigateOptions = {
|
|
90
|
+
/**
|
|
91
|
+
Replace the current entry in the navigation stack instead of pushing a new
|
|
92
|
+
one.
|
|
93
|
+
*/
|
|
94
|
+
replace?: boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
Arbitrary state to attach to the destination location, readable via
|
|
98
|
+
`useLocation().state`. Held in memory, never serialized.
|
|
99
|
+
*/
|
|
100
|
+
state?: unknown;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
Navigates to a destination, or through the navigation stack when passed a
|
|
105
|
+
number (`navigate(-1)` goes back).
|
|
106
|
+
*/
|
|
107
|
+
export type NavigateFunction = {
|
|
108
|
+
(to: To, options?: NavigateOptions): void;
|
|
109
|
+
(delta: number): void;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
Returns a stable function for imperative navigation.
|
|
114
|
+
*/
|
|
115
|
+
export const useNavigate = (): NavigateFunction => {
|
|
116
|
+
const { navigator } = useNavigationContext("useNavigate()");
|
|
117
|
+
const { matches } = useContext(RouteContext);
|
|
118
|
+
const { pathname: locationPathname } = useLocation();
|
|
119
|
+
|
|
120
|
+
const routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
|
|
121
|
+
|
|
122
|
+
// Navigating before the component has mounted (i.e. during render) is a
|
|
123
|
+
// side effect in render; ignore it. Use <Navigate> or an effect instead.
|
|
124
|
+
const activeRef = useRef(false);
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
activeRef.current = true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return useCallback(
|
|
130
|
+
(to: To | number, options: NavigateOptions = {}) => {
|
|
131
|
+
if (!activeRef.current) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof to === "number") {
|
|
136
|
+
navigator.go(to);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const path = resolveTo(to, JSON.parse(routePathnamesJson) as string[], locationPathname);
|
|
141
|
+
(options.replace ? navigator.replace : navigator.push)(path, options.state);
|
|
142
|
+
},
|
|
143
|
+
[navigator, routePathnamesJson, locationPathname],
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
Returns whether the navigation stack has entries behind/ahead of the current
|
|
149
|
+
one — i.e. whether `navigate(-1)` / `navigate(1)` will move anywhere. Handy
|
|
150
|
+
for "Esc goes back, unless at root" bindings.
|
|
151
|
+
*/
|
|
152
|
+
export const useNavigationStack = (): { canGoBack: boolean; canGoForward: boolean } => {
|
|
153
|
+
const { navigator } = useNavigationContext("useNavigationStack()");
|
|
154
|
+
// Reading location subscribes this component to navigation changes, so the
|
|
155
|
+
// returned booleans stay fresh.
|
|
156
|
+
useLocation();
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
canGoBack: navigator.canGoBack(),
|
|
160
|
+
canGoForward: navigator.canGoForward(),
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
Returns the params from all dynamic segments matched by the current route and
|
|
166
|
+
its ancestors.
|
|
167
|
+
*/
|
|
168
|
+
export const useParams = <
|
|
169
|
+
ParamsOrKey extends Record<string, string | undefined> | string = string,
|
|
170
|
+
>(): Readonly<[ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>> => {
|
|
171
|
+
const { matches } = useContext(RouteContext);
|
|
172
|
+
const routeMatch = matches[matches.length - 1];
|
|
173
|
+
return (routeMatch ? routeMatch.params : {}) as Readonly<
|
|
174
|
+
[ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>
|
|
175
|
+
>;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
Matches a path pattern against the current location's pathname. Returns the
|
|
180
|
+
match (with params) or `null`.
|
|
181
|
+
*/
|
|
182
|
+
export const useMatch = <ParamKey extends string = string>(
|
|
183
|
+
pattern: PathPattern | string,
|
|
184
|
+
): PathMatch<ParamKey> | null => {
|
|
185
|
+
const { pathname } = useLocation();
|
|
186
|
+
return matchPath<ParamKey>(pattern, pathname);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
Resolves a `To` value against the current route, exactly as `useNavigate`
|
|
191
|
+
would. Useful for building navigation UI.
|
|
192
|
+
*/
|
|
193
|
+
export const useResolvedPath = (to: To): Path => {
|
|
194
|
+
const { matches } = useContext(RouteContext);
|
|
195
|
+
const { pathname: locationPathname } = useLocation();
|
|
196
|
+
return resolveTo(to, getResolveToMatches(matches), locationPathname);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
Returns the element for the child route at this level of the route hierarchy,
|
|
201
|
+
or `null` if there is none. Used internally by `<Outlet>`.
|
|
202
|
+
*/
|
|
203
|
+
export const useOutlet = (context?: unknown): ReactElement | null => {
|
|
204
|
+
const { outlet } = useContext(RouteContext);
|
|
205
|
+
if (outlet) {
|
|
206
|
+
return <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return outlet;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
Returns the value passed to the nearest parent `<Outlet context={...}>`.
|
|
214
|
+
*/
|
|
215
|
+
export const useOutletContext = <Context = unknown,>(): Context =>
|
|
216
|
+
useContext(OutletContext) as Context;
|
|
217
|
+
|
|
218
|
+
type SearchParamsInit = string | string[][] | Record<string, string | string[]> | URLSearchParams;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
Creates a `URLSearchParams` from common initializer shapes, including
|
|
222
|
+
`{ key: ["a", "b"] }` for repeated keys.
|
|
223
|
+
*/
|
|
224
|
+
export const createSearchParams = (init: SearchParamsInit = ""): URLSearchParams =>
|
|
225
|
+
new URLSearchParams(
|
|
226
|
+
typeof init === "string" || Array.isArray(init) || init instanceof URLSearchParams
|
|
227
|
+
? init
|
|
228
|
+
: Object.keys(init).flatMap((key) => {
|
|
229
|
+
const value = init[key];
|
|
230
|
+
return Array.isArray(value)
|
|
231
|
+
? value.map((v) => [key, v] as [string, string])
|
|
232
|
+
: [[key, value] as [string, string]];
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
type SetSearchParams = (
|
|
237
|
+
nextInit: SearchParamsInit | ((prev: URLSearchParams) => SearchParamsInit),
|
|
238
|
+
navigateOptions?: NavigateOptions,
|
|
239
|
+
) => void;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
Returns the current location's search params and a setter that navigates to
|
|
243
|
+
the same pathname with the new params.
|
|
244
|
+
*/
|
|
245
|
+
export const useSearchParams = (
|
|
246
|
+
defaultInit?: SearchParamsInit,
|
|
247
|
+
): [URLSearchParams, SetSearchParams] => {
|
|
248
|
+
const defaultSearchParamsRef = useRef(createSearchParams(defaultInit));
|
|
249
|
+
const hasSetSearchParamsRef = useRef(false);
|
|
250
|
+
|
|
251
|
+
const location = useLocation();
|
|
252
|
+
const searchParams = useMemo(() => {
|
|
253
|
+
const params = createSearchParams(location.search);
|
|
254
|
+
|
|
255
|
+
// Fill in defaults only until the setter is first used, so cleared params
|
|
256
|
+
// don't resurrect their default values.
|
|
257
|
+
if (!hasSetSearchParamsRef.current) {
|
|
258
|
+
for (const key of defaultSearchParamsRef.current.keys()) {
|
|
259
|
+
if (!params.has(key)) {
|
|
260
|
+
for (const value of defaultSearchParamsRef.current.getAll(key)) {
|
|
261
|
+
params.append(key, value);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return params;
|
|
268
|
+
}, [location.search]);
|
|
269
|
+
|
|
270
|
+
const navigate = useNavigate();
|
|
271
|
+
const setSearchParams = useCallback<SetSearchParams>(
|
|
272
|
+
(nextInit, navigateOptions) => {
|
|
273
|
+
const newSearchParams = createSearchParams(
|
|
274
|
+
typeof nextInit === "function" ? nextInit(new URLSearchParams(searchParams)) : nextInit,
|
|
275
|
+
);
|
|
276
|
+
hasSetSearchParamsRef.current = true;
|
|
277
|
+
navigate(`?${newSearchParams.toString()}`, navigateOptions);
|
|
278
|
+
},
|
|
279
|
+
[navigate, searchParams],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return [searchParams, setSearchParams];
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
Matches a set of route objects against the current location (or an override)
|
|
287
|
+
and returns the rendered element tree. The plain-object alternative to
|
|
288
|
+
`<Routes>`.
|
|
289
|
+
*/
|
|
290
|
+
export const useRoutes = (
|
|
291
|
+
routes: RouteObject[],
|
|
292
|
+
locationArg?: Partial<Location> | string,
|
|
293
|
+
): ReactElement | null => {
|
|
294
|
+
if (!useInRouterContext()) {
|
|
295
|
+
throw new Error("useRoutes() may be used only in the context of a <MemoryRouter> component.");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { matches: parentMatches } = useContext(RouteContext);
|
|
299
|
+
const routeMatch = parentMatches[parentMatches.length - 1];
|
|
300
|
+
const parentParams = routeMatch ? routeMatch.params : {};
|
|
301
|
+
const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
|
|
302
|
+
const parentRoute = routeMatch?.route;
|
|
303
|
+
|
|
304
|
+
const locationFromContext = useLocation();
|
|
305
|
+
|
|
306
|
+
let location;
|
|
307
|
+
if (locationArg) {
|
|
308
|
+
const parsedLocationArg =
|
|
309
|
+
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
|
|
310
|
+
|
|
311
|
+
if (parentPathnameBase !== "/" && !parsedLocationArg.pathname?.startsWith(parentPathnameBase)) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, ` +
|
|
314
|
+
`the location pathname must begin with the portion of the pathname that was matched by ` +
|
|
315
|
+
`all parent routes. The current pathname base is "${parentPathnameBase}" but pathname ` +
|
|
316
|
+
`"${parsedLocationArg.pathname}" was given in the \`location\` prop.`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
location = parsedLocationArg;
|
|
321
|
+
} else {
|
|
322
|
+
location = locationFromContext;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const pathname = location.pathname ?? "/";
|
|
326
|
+
|
|
327
|
+
let remainingPathname = pathname;
|
|
328
|
+
if (parentPathnameBase !== "/") {
|
|
329
|
+
const parentSegments = parentPathnameBase.replace(/^\//, "").split("/");
|
|
330
|
+
const segments = pathname.replace(/^\//, "").split("/");
|
|
331
|
+
remainingPathname = `/${segments.slice(parentSegments.length).join("/")}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const matches = matchRoutes(routes, { pathname: remainingPathname });
|
|
335
|
+
|
|
336
|
+
// Descendant <Routes> under a parent route without a trailing "*" can never
|
|
337
|
+
// see deeper paths — the parent stops matching as soon as the location goes
|
|
338
|
+
// past its own path.
|
|
339
|
+
const parentPath = parentRoute?.path ?? "";
|
|
340
|
+
if (parentRoute && !parentPath.endsWith("*")) {
|
|
341
|
+
warnOnce(
|
|
342
|
+
`descendant-routes:${routeMatch.pathname}`,
|
|
343
|
+
`You rendered descendant <Routes> (or called useRoutes()) at "${routeMatch.pathname}" ` +
|
|
344
|
+
`(under <Route path="${parentPath}">) but the parent route path has no trailing "*". ` +
|
|
345
|
+
`This means if you navigate deeper, the parent won't match anymore and therefore the ` +
|
|
346
|
+
`child routes will never render. Please change the parent to <Route path="${
|
|
347
|
+
parentPath === "/" ? "*" : `${parentPath}/*`
|
|
348
|
+
}">.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (matches == null && !parentRoute) {
|
|
353
|
+
warnOnce(
|
|
354
|
+
`no-match:${pathname}${location.search ?? ""}`,
|
|
355
|
+
`No routes matched location "${pathname}${location.search ?? ""}".`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return renderMatches(
|
|
360
|
+
matches &&
|
|
361
|
+
matches.map((match) => ({
|
|
362
|
+
...match,
|
|
363
|
+
params: { ...parentParams, ...match.params },
|
|
364
|
+
pathname: joinPaths([parentPathnameBase, match.pathname]),
|
|
365
|
+
pathnameBase:
|
|
366
|
+
match.pathnameBase === "/"
|
|
367
|
+
? parentPathnameBase
|
|
368
|
+
: joinPaths([parentPathnameBase, match.pathnameBase]),
|
|
369
|
+
})),
|
|
370
|
+
parentMatches,
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
function renderMatches(
|
|
375
|
+
matches: RouteMatch[] | null,
|
|
376
|
+
parentMatches: RouteMatch[],
|
|
377
|
+
): ReactElement | null {
|
|
378
|
+
if (matches == null) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return matches.reduceRight<ReactElement | null>((outlet, match, index) => {
|
|
383
|
+
const matchesUpToHere = parentMatches.concat(matches.slice(0, index + 1));
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<RouteContext.Provider value={{ outlet, matches: matchesUpToHere }}>
|
|
387
|
+
{match.route.element ?? outlet}
|
|
388
|
+
</RouteContext.Provider>
|
|
389
|
+
);
|
|
390
|
+
}, null);
|
|
391
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// A purpose-built in-memory router for terminal apps. The matching engine and
|
|
2
|
+
// component model are ported from react-router's declarative mode (MIT), with
|
|
3
|
+
// everything URL- and DOM-specific removed: no basename, no hash, no percent
|
|
4
|
+
// encoding, no data APIs. Route paths are screen states, not URLs.
|
|
5
|
+
export type { InitialEntry, Location, NavigationType, Path, To } from "#/router/history.ts";
|
|
6
|
+
export { createPath, parsePath } from "#/router/history.ts";
|
|
7
|
+
export type { Params, PathMatch, PathPattern, RouteMatch, RouteObject } from "#/router/matcher.ts";
|
|
8
|
+
export { generatePath, matchPath, matchRoutes, resolvePath } from "#/router/matcher.ts";
|
|
9
|
+
export type { Navigator } from "#/router/context.ts";
|
|
10
|
+
export type { NavigateFunction, NavigateOptions } from "#/router/hooks.tsx";
|
|
11
|
+
export {
|
|
12
|
+
createSearchParams,
|
|
13
|
+
useInRouterContext,
|
|
14
|
+
useLocation,
|
|
15
|
+
useMatch,
|
|
16
|
+
useNavigate,
|
|
17
|
+
useNavigationStack,
|
|
18
|
+
useNavigationType,
|
|
19
|
+
useOutlet,
|
|
20
|
+
useOutletContext,
|
|
21
|
+
useParams,
|
|
22
|
+
useResolvedPath,
|
|
23
|
+
useRoutes,
|
|
24
|
+
useSearchParams,
|
|
25
|
+
} from "#/router/hooks.tsx";
|
|
26
|
+
export type {
|
|
27
|
+
LinkProps,
|
|
28
|
+
MemoryRouterProps,
|
|
29
|
+
NavigateProps,
|
|
30
|
+
OutletProps,
|
|
31
|
+
RouteProps,
|
|
32
|
+
RoutesProps,
|
|
33
|
+
} from "#/router/components.tsx";
|
|
34
|
+
export { Link, MemoryRouter, Navigate, Outlet, Route, Routes } from "#/router/components.tsx";
|