@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5

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 (207) hide show
  1. package/README.md +23 -13
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-D5HUf3Fj.d.ts +452 -0
  4. package/dist/ansi.d.ts +137 -143
  5. package/dist/ansi.js +87 -2
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-Bmc2tRPk.d.ts +21 -0
  24. package/dist/index.d.ts +235 -754
  25. package/dist/index.js +1362 -3001
  26. package/dist/osc-CCH7xDoS.js +71 -0
  27. package/dist/osc-Cn0fw77g.d.ts +23 -0
  28. package/dist/paint-C19minOS.d.ts +81 -0
  29. package/dist/query-vaIeGOkH.d.ts +152 -0
  30. package/dist/router.d.ts +392 -0
  31. package/dist/router.js +709 -0
  32. package/dist/sample-Cqw1bjUL.js +445 -0
  33. package/dist/screen-BOh__-65.js +324 -0
  34. package/dist/screen-BReKIheE.d.ts +40 -0
  35. package/dist/screen.d.ts +5 -0
  36. package/dist/screen.js +5 -0
  37. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  38. package/dist/serialize-BTkAZgw1.js +79 -0
  39. package/dist/session-BJmzX2NJ.js +664 -0
  40. package/dist/sgr-BhwaWAJB.js +246 -0
  41. package/dist/store-CgrG9K4y.d.ts +72 -0
  42. package/dist/string-width-CijQwpIk.js +69 -0
  43. package/dist/strip-BvU4toXG.js +6 -0
  44. package/dist/terminal.d.ts +118 -0
  45. package/dist/terminal.js +2 -0
  46. package/dist/tokenize-AjqbvtiT.js +1242 -0
  47. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  48. package/dist/truncate-D31fhU6i.js +562 -0
  49. package/dist/use-focus-C2sciZOo.js +1335 -0
  50. package/package.json +43 -17
  51. package/src/ansi/chalk.ts +12 -53
  52. package/src/ansi/cursor.ts +2 -4
  53. package/src/ansi/east-asian-width.ts +44 -0
  54. package/src/ansi/escapes.ts +18 -26
  55. package/src/ansi/graphemes.ts +8 -0
  56. package/src/ansi/hyperlink.ts +44 -0
  57. package/src/ansi/index.ts +14 -14
  58. package/src/ansi/osc.ts +77 -0
  59. package/src/ansi/sgr.ts +2 -5
  60. package/src/ansi/slice.ts +1 -1
  61. package/src/ansi/string-width.ts +91 -206
  62. package/src/ansi/strip.ts +13 -33
  63. package/src/ansi/tokenize.ts +96 -169
  64. package/src/ansi/truncate.ts +5 -3
  65. package/src/ansi/wrap.ts +13 -14
  66. package/src/ansi-tokenizer.ts +1 -1
  67. package/src/capabilities/color-policy.ts +34 -0
  68. package/src/capabilities/detect.ts +594 -0
  69. package/src/capabilities/index.ts +37 -0
  70. package/src/capabilities/query.ts +657 -0
  71. package/src/capabilities/store.ts +379 -0
  72. package/src/color/index.ts +3 -0
  73. package/src/color/paint.ts +169 -0
  74. package/src/color/palette.ts +48 -0
  75. package/src/color/sample.ts +323 -0
  76. package/src/color.ts +1 -0
  77. package/src/components/AnsiText.tsx +42 -0
  78. package/src/components/App.tsx +109 -22
  79. package/src/components/BackgroundContext.ts +2 -3
  80. package/src/components/Box.tsx +43 -60
  81. package/src/components/CursorContext.ts +1 -1
  82. package/src/components/ErrorBoundary.tsx +1 -1
  83. package/src/components/ErrorOverview.tsx +8 -7
  84. package/src/components/Hyperlink.tsx +56 -0
  85. package/src/components/Spacer.tsx +1 -1
  86. package/src/components/Static.tsx +1 -1
  87. package/src/components/StderrContext.ts +0 -2
  88. package/src/components/StdinContext.ts +0 -1
  89. package/src/components/StdoutContext.ts +1 -3
  90. package/src/components/TerminalOscContext.ts +25 -0
  91. package/src/components/Text.tsx +23 -46
  92. package/src/components/Transform.tsx +2 -2
  93. package/src/cursor-position.ts +1 -1
  94. package/src/devtools.ts +84 -24
  95. package/src/dom.ts +11 -8
  96. package/src/env.ts +12 -0
  97. package/src/get-max-width.ts +1 -1
  98. package/src/global.d.ts +6 -4
  99. package/src/{boxes.ts → glyphs.ts} +17 -18
  100. package/src/hooks/use-animation.ts +1 -1
  101. package/src/hooks/use-app.ts +1 -1
  102. package/src/hooks/use-box-metrics.ts +1 -1
  103. package/src/hooks/use-capabilities.ts +73 -0
  104. package/src/hooks/use-cursor.ts +3 -3
  105. package/src/hooks/use-focus-manager.ts +1 -1
  106. package/src/hooks/use-focus.ts +5 -6
  107. package/src/hooks/use-input.ts +4 -4
  108. package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
  109. package/src/hooks/use-paste.ts +2 -2
  110. package/src/hooks/use-stderr.ts +1 -1
  111. package/src/hooks/use-stdin.ts +1 -1
  112. package/src/hooks/use-stdout.ts +1 -1
  113. package/src/hooks/use-terminal-osc.ts +59 -0
  114. package/src/hooks/use-window-size.ts +2 -2
  115. package/src/index.ts +88 -44
  116. package/src/ink.tsx +620 -745
  117. package/src/input-parser.ts +1 -1
  118. package/src/instances.ts +1 -1
  119. package/src/kitty-keyboard.ts +128 -0
  120. package/src/measure-element.ts +1 -1
  121. package/src/measure-text.ts +2 -2
  122. package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
  123. package/src/parse-keypress.ts +4 -5
  124. package/src/parse-stack-line.ts +0 -1
  125. package/src/patch-console.ts +44 -0
  126. package/src/reconciler.ts +36 -62
  127. package/src/render-background.ts +37 -16
  128. package/src/render-border.ts +97 -64
  129. package/src/render-frame.ts +83 -0
  130. package/src/render-to-string.ts +24 -9
  131. package/src/render.ts +24 -16
  132. package/src/router/components.tsx +343 -0
  133. package/src/router/context.ts +41 -0
  134. package/src/router/history.ts +194 -0
  135. package/src/router/hooks.tsx +391 -0
  136. package/src/router/index.ts +34 -0
  137. package/src/router/matcher.ts +571 -0
  138. package/src/sanitize-ansi.ts +1 -1
  139. package/src/screen/ansi.ts +184 -0
  140. package/src/screen/canvas.ts +137 -0
  141. package/src/screen/cell.ts +138 -0
  142. package/src/screen/color-profile.ts +47 -0
  143. package/src/screen/geometry.ts +9 -0
  144. package/src/screen/index.ts +6 -0
  145. package/src/screen/screen.ts +272 -0
  146. package/src/screen/serialize.ts +129 -0
  147. package/src/screen.ts +1 -0
  148. package/src/semantic-text-style.ts +118 -0
  149. package/src/signal-exit.ts +4 -5
  150. package/src/squash-text-nodes.ts +4 -7
  151. package/src/stream.ts +2 -4
  152. package/src/structured-text.ts +321 -0
  153. package/src/styles.ts +16 -17
  154. package/src/terminal/index.ts +2 -0
  155. package/src/terminal/inline-presenter.ts +120 -0
  156. package/src/terminal/input.ts +86 -0
  157. package/src/terminal/render-scheduler.ts +37 -0
  158. package/src/terminal/screen-presenter.ts +188 -0
  159. package/src/terminal/session.ts +407 -0
  160. package/src/terminal-size.ts +9 -8
  161. package/src/terminal.ts +1 -0
  162. package/src/testing/browser.ts +588 -0
  163. package/src/testing/emulators.ts +205 -0
  164. package/src/testing/explorer-app/index.html +12 -0
  165. package/src/testing/explorer-app/main.ts +381 -0
  166. package/src/testing/explorer-app/style.css +194 -0
  167. package/src/testing/explorer-app/tsconfig.json +15 -0
  168. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  169. package/src/testing/index.ts +26 -0
  170. package/src/testing/keys.ts +56 -0
  171. package/src/testing/live.ts +85 -0
  172. package/src/testing/matchers.ts +70 -0
  173. package/src/testing/public.ts +94 -0
  174. package/src/testing/terminal.ts +349 -0
  175. package/src/testing/vitest.ts +157 -0
  176. package/src/transform-adapter.ts +14 -0
  177. package/src/types.ts +1 -6
  178. package/src/utils.ts +2 -2
  179. package/src/wrap-text.ts +4 -4
  180. package/src/yoga/config.ts +2 -2
  181. package/src/yoga/core/absoluteLayout.ts +15 -15
  182. package/src/yoga/core/baseline.ts +3 -3
  183. package/src/yoga/core/cache.ts +4 -4
  184. package/src/yoga/core/calculateLayout.ts +33 -27
  185. package/src/yoga/core/config.ts +1 -1
  186. package/src/yoga/core/flexLine.ts +3 -3
  187. package/src/yoga/core/helpers.ts +3 -3
  188. package/src/yoga/core/layoutResults.ts +4 -4
  189. package/src/yoga/core/node.ts +16 -16
  190. package/src/yoga/core/pixelGrid.ts +4 -4
  191. package/src/yoga/core/style.ts +11 -11
  192. package/src/yoga/core/types.ts +2 -2
  193. package/src/yoga/index.ts +6 -6
  194. package/src/yoga/node.ts +8 -8
  195. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  196. package/dist/truncate-CBiyyZzw.js +0 -2156
  197. package/src/ansi/supports-color.ts +0 -213
  198. package/src/ansi/widest-line.ts +0 -12
  199. package/src/auto-bind.ts +0 -41
  200. package/src/colorize.ts +0 -60
  201. package/src/devtools-window-polyfill.ts +0 -73
  202. package/src/indent-string.ts +0 -16
  203. package/src/is-in-ci.ts +0 -7
  204. package/src/log-update.ts +0 -370
  205. package/src/output.ts +0 -308
  206. package/src/renderer.ts +0 -73
  207. package/src/write-synchronized.ts +0 -9
@@ -0,0 +1,343 @@
1
+ /** @jsxImportSource react */
2
+ // MemoryRouter/Routes/Route/Navigate/Outlet are ported from react-router's
3
+ // declarative components (lib/components.tsx, MIT). Link is Sigil-native.
4
+ import {
5
+ Children,
6
+ Fragment,
7
+ isValidElement,
8
+ startTransition,
9
+ useCallback,
10
+ useEffect,
11
+ useLayoutEffect,
12
+ useMemo,
13
+ useRef,
14
+ useState,
15
+ type ReactElement,
16
+ type ReactNode,
17
+ } from "react";
18
+
19
+ import { Text, type Props as TextProps } from "#/components/Text.tsx";
20
+ import { useFocus } from "#/hooks/use-focus.ts";
21
+ import { useInput } from "#/hooks/use-input.ts";
22
+ import { LocationContext, NavigationContext, type Navigator } from "#/router/context.ts";
23
+ import {
24
+ createMemoryHistory,
25
+ type InitialEntry,
26
+ type Location,
27
+ type To,
28
+ } from "#/router/history.ts";
29
+ import {
30
+ useLocation,
31
+ useNavigate,
32
+ useOutlet,
33
+ useResolvedPath,
34
+ useRoutes,
35
+ } from "#/router/hooks.tsx";
36
+ import { normalizePathname, type RouteObject } from "#/router/matcher.ts";
37
+
38
+ export type MemoryRouterProps = {
39
+ /**
40
+ The navigation stack to start with. Defaults to `["/"]`.
41
+ */
42
+ initialEntries?: InitialEntry[];
43
+
44
+ /**
45
+ The index of the initial entry to render. Defaults to the last entry.
46
+ */
47
+ initialIndex?: number;
48
+
49
+ children?: ReactNode;
50
+ };
51
+
52
+ /**
53
+ The routing container for a Sigil app. Stores the navigation stack in memory —
54
+ routes aren't URLs, they're screen states.
55
+
56
+ ```tsx
57
+ <MemoryRouter>
58
+ <Routes>
59
+ <Route path="/" element={<Home />} />
60
+ <Route path="/settings" element={<Settings />} />
61
+ </Routes>
62
+ </MemoryRouter>
63
+ ```
64
+ */
65
+ export function MemoryRouter({ initialEntries, initialIndex, children }: MemoryRouterProps) {
66
+ const historyRef = useRef<ReturnType<typeof createMemoryHistory> | null>(null);
67
+ historyRef.current ??= createMemoryHistory({ initialEntries, initialIndex });
68
+
69
+ const history = historyRef.current;
70
+ const [state, setState] = useState({
71
+ action: history.action,
72
+ location: history.location,
73
+ });
74
+
75
+ // Navigation is a transition: if the destination screen suspends, the
76
+ // current screen stays visible until it's ready.
77
+ useLayoutEffect(
78
+ () =>
79
+ history.listen(({ action, location }) => {
80
+ startTransition(() => {
81
+ setState({ action, location });
82
+ });
83
+ }),
84
+ [history],
85
+ );
86
+
87
+ const navigator = useMemo<Navigator>(
88
+ () => ({
89
+ push: (to, historyState) => history.push(to, historyState),
90
+ replace: (to, historyState) => history.replace(to, historyState),
91
+ go: (delta) => history.go(delta),
92
+ canGoBack: () => history.canGoBack,
93
+ canGoForward: () => history.canGoForward,
94
+ }),
95
+ [history],
96
+ );
97
+
98
+ const navigationContext = useMemo(() => ({ navigator }), [navigator]);
99
+ const locationContext = useMemo(
100
+ () => ({ location: state.location, navigationType: state.action }),
101
+ [state],
102
+ );
103
+
104
+ return (
105
+ <NavigationContext.Provider value={navigationContext}>
106
+ <LocationContext.Provider value={locationContext}>{children}</LocationContext.Provider>
107
+ </NavigationContext.Provider>
108
+ );
109
+ }
110
+
111
+ export type RouteProps = {
112
+ /**
113
+ The path pattern to match, relative to the parent route. Supports `:param`
114
+ dynamic segments, optional segments (`:param?`, `edit?`), and a trailing
115
+ `*` splat.
116
+ */
117
+ path?: string;
118
+
119
+ /**
120
+ Render this route in the parent's `<Outlet>` at the parent's exact path.
121
+ Index routes cannot have children.
122
+ */
123
+ index?: boolean;
124
+
125
+ /**
126
+ The element to render when this route matches.
127
+ */
128
+ element?: ReactNode;
129
+
130
+ /**
131
+ Nested `<Route>` elements, rendered into this route's `<Outlet>`.
132
+ */
133
+ children?: ReactNode;
134
+ };
135
+
136
+ /**
137
+ Declares a route. Only valid as a child of `<Routes>` or another `<Route>`.
138
+ */
139
+ export function Route(_props: RouteProps): ReactElement | null {
140
+ throw new Error(
141
+ "A <Route> is only ever to be used as the child of a <Routes> element, " +
142
+ "never rendered directly. Please wrap your <Route> in a <Routes>.",
143
+ );
144
+ }
145
+
146
+ function createRoutesFromChildren(children: ReactNode): RouteObject[] {
147
+ const routes: RouteObject[] = [];
148
+
149
+ Children.forEach(children, (element) => {
150
+ if (!isValidElement(element)) {
151
+ // Ignore non-elements. This allows people to more easily inline
152
+ // conditionals in their route config.
153
+ return;
154
+ }
155
+
156
+ if (element.type === Fragment) {
157
+ // Transparently support React.Fragment and its children.
158
+ routes.push(
159
+ ...createRoutesFromChildren((element.props as { children?: ReactNode }).children),
160
+ );
161
+ return;
162
+ }
163
+
164
+ if (element.type !== Route) {
165
+ throw new Error(
166
+ `[${typeof element.type === "string" ? element.type : ((element.type as { name?: string }).name ?? "unknown")}] ` +
167
+ `is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>.`,
168
+ );
169
+ }
170
+
171
+ const props = element.props as RouteProps;
172
+ const route: RouteObject = {
173
+ path: props.path,
174
+ index: props.index,
175
+ element: props.element,
176
+ };
177
+
178
+ if (props.children) {
179
+ route.children = createRoutesFromChildren(props.children);
180
+ }
181
+
182
+ routes.push(route);
183
+ });
184
+
185
+ return routes;
186
+ }
187
+
188
+ export type RoutesProps = {
189
+ children?: ReactNode;
190
+
191
+ /**
192
+ Match against this location instead of the current one. Useful for
193
+ rendering a screen other than the one navigated to (e.g. transitions).
194
+ */
195
+ location?: Partial<Location> | string;
196
+ };
197
+
198
+ /**
199
+ Renders the branch of child `<Route>` elements that best matches the current
200
+ location.
201
+ */
202
+ export function Routes({ children, location }: RoutesProps): ReactElement | null {
203
+ return useRoutes(createRoutesFromChildren(children), location);
204
+ }
205
+
206
+ export type OutletProps = {
207
+ /**
208
+ A value to make available to descendant routes via `useOutletContext()`.
209
+ */
210
+ context?: unknown;
211
+ };
212
+
213
+ /**
214
+ Renders the matching child route of a parent route, or nothing if no child
215
+ matches.
216
+ */
217
+ export function Outlet(props: OutletProps): ReactElement | null {
218
+ return useOutlet(props.context);
219
+ }
220
+
221
+ export type NavigateProps = {
222
+ to: To;
223
+ replace?: boolean;
224
+ state?: unknown;
225
+ };
226
+
227
+ /**
228
+ Navigates as soon as it renders. The component form of `useNavigate`, for
229
+ declarative redirects:
230
+
231
+ ```tsx
232
+ <Route path="/" element={<Navigate to="/home" replace />} />
233
+ ```
234
+ */
235
+ export function Navigate({ to, replace, state }: NavigateProps): null {
236
+ const navigate = useNavigate();
237
+ const { pathname, search } = useResolvedPath(to);
238
+
239
+ useEffect(() => {
240
+ navigate({ pathname, search }, { replace, state });
241
+ }, [navigate, pathname, search, replace, state]);
242
+
243
+ return null;
244
+ }
245
+
246
+ type LinkRenderState = {
247
+ /**
248
+ Whether this link currently has focus.
249
+ */
250
+ isFocused: boolean;
251
+
252
+ /**
253
+ Whether the current location is the link's destination or a descendant of
254
+ it.
255
+ */
256
+ isActive: boolean;
257
+ };
258
+
259
+ export type LinkProps = Omit<TextProps, "children"> & {
260
+ /**
261
+ The destination to navigate to when the link is activated.
262
+ */
263
+ to: To;
264
+
265
+ /**
266
+ Replace the current entry in the navigation stack instead of pushing.
267
+ */
268
+ replace?: boolean;
269
+
270
+ /**
271
+ State to attach to the destination location.
272
+ */
273
+ state?: unknown;
274
+
275
+ /**
276
+ Focus this link if nothing else is focused yet.
277
+ */
278
+ autoFocus?: boolean;
279
+
280
+ /**
281
+ An ID for programmatic focus via `useFocusManager().focus(id)`.
282
+ */
283
+ id?: string;
284
+
285
+ /**
286
+ Link content. Pass a function to take full control of rendering based on
287
+ focus and active state.
288
+ */
289
+ children?: ReactNode | ((state: LinkRenderState) => ReactNode);
290
+ };
291
+
292
+ /**
293
+ A focusable navigation element — the terminal's `<a>` tag. Focus it with
294
+ <kbd>Tab</kbd> and activate it with <kbd>Enter</kbd>. By default the focused
295
+ link renders inverse; pass a function as `children` (or any `Text` props) to
296
+ customize.
297
+
298
+ ```tsx
299
+ <Link to="/settings">Settings</Link>
300
+ ```
301
+ */
302
+ export function Link({
303
+ to,
304
+ replace = false,
305
+ state,
306
+ autoFocus = false,
307
+ id,
308
+ children,
309
+ ...textProps
310
+ }: LinkProps) {
311
+ const navigate = useNavigate();
312
+ const path = useResolvedPath(to);
313
+ const { pathname: locationPathname } = useLocation();
314
+ const { isFocused } = useFocus({ autoFocus, id });
315
+
316
+ const toPathname = normalizePathname(path.pathname);
317
+ const isActive =
318
+ locationPathname === toPathname ||
319
+ (locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === "/");
320
+
321
+ const activate = useCallback(() => {
322
+ navigate({ pathname: path.pathname, search: path.search }, { replace, state });
323
+ }, [navigate, path.pathname, path.search, replace, state]);
324
+
325
+ useInput(
326
+ (_input, key) => {
327
+ if (key.return) {
328
+ activate();
329
+ }
330
+ },
331
+ { isActive: isFocused },
332
+ );
333
+
334
+ if (typeof children === "function") {
335
+ return <>{children({ isFocused, isActive })}</>;
336
+ }
337
+
338
+ return (
339
+ <Text inverse={isFocused} {...textProps}>
340
+ {children}
341
+ </Text>
342
+ );
343
+ }
@@ -0,0 +1,41 @@
1
+ import { createContext, type ReactElement } from "react";
2
+
3
+ import { type Location, type NavigationType, type Path } from "#/router/history.ts";
4
+ import { type RouteMatch } from "#/router/matcher.ts";
5
+
6
+ /**
7
+ The imperative interface `useNavigate` drives. Backed by the memory history
8
+ inside `<MemoryRouter>`.
9
+ */
10
+ export type Navigator = {
11
+ push: (to: Path, state?: unknown) => void;
12
+ replace: (to: Path, state?: unknown) => void;
13
+ go: (delta: number) => void;
14
+ canGoBack: () => boolean;
15
+ canGoForward: () => boolean;
16
+ };
17
+
18
+ export type NavigationContextObject = {
19
+ navigator: Navigator;
20
+ };
21
+
22
+ export const NavigationContext = createContext<NavigationContextObject | null>(null);
23
+
24
+ export type LocationContextObject = {
25
+ location: Location;
26
+ navigationType: NavigationType;
27
+ };
28
+
29
+ export const LocationContext = createContext<LocationContextObject | null>(null);
30
+
31
+ export type RouteContextObject = {
32
+ outlet: ReactElement | null;
33
+ matches: RouteMatch[];
34
+ };
35
+
36
+ export const RouteContext = createContext<RouteContextObject>({
37
+ outlet: null,
38
+ matches: [],
39
+ });
40
+
41
+ export const OutletContext = createContext<unknown>(null);
@@ -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
+ };