@real-router/core 0.50.2 → 0.52.0

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 (58) hide show
  1. package/dist/cjs/Router-DrBkBdZ5.d.ts.map +1 -1
  2. package/dist/cjs/Router-Pztue5fk.js +6 -0
  3. package/dist/cjs/Router-Pztue5fk.js.map +1 -0
  4. package/dist/{esm/RouterValidator-BLtjhvRo.d.mts → cjs/RouterValidator-DLy_W2du.d.ts} +2 -1
  5. package/dist/cjs/{RouterValidator-BL1Uq6Rq.d.ts.map → RouterValidator-DLy_W2du.d.ts.map} +1 -1
  6. package/dist/cjs/api.d.ts.map +1 -1
  7. package/dist/cjs/api.js +1 -1
  8. package/dist/cjs/api.js.map +1 -1
  9. package/dist/cjs/getPluginApi-CUcFDzuA.js +2 -0
  10. package/dist/cjs/getPluginApi-CUcFDzuA.js.map +1 -0
  11. package/dist/cjs/index.d.ts +1 -1
  12. package/dist/cjs/index.js +1 -1
  13. package/dist/cjs/internals-na15rxo_.js.map +1 -1
  14. package/dist/cjs/utils.d.ts +70 -2
  15. package/dist/cjs/utils.d.ts.map +1 -1
  16. package/dist/cjs/utils.js +1 -1
  17. package/dist/cjs/utils.js.map +1 -1
  18. package/dist/cjs/validation.d.ts +9 -2
  19. package/dist/cjs/validation.d.ts.map +1 -1
  20. package/dist/esm/Router-BeXr2zW4.d.mts.map +1 -1
  21. package/dist/esm/Router-CK8U23pP.mjs +6 -0
  22. package/dist/esm/Router-CK8U23pP.mjs.map +1 -0
  23. package/dist/{cjs/RouterValidator-BL1Uq6Rq.d.ts → esm/RouterValidator-C-PvV00i.d.mts} +2 -1
  24. package/dist/esm/{RouterValidator-BLtjhvRo.d.mts.map → RouterValidator-C-PvV00i.d.mts.map} +1 -1
  25. package/dist/esm/api.d.mts.map +1 -1
  26. package/dist/esm/api.mjs +1 -1
  27. package/dist/esm/api.mjs.map +1 -1
  28. package/dist/esm/getPluginApi-CsTfDB-O.mjs +2 -0
  29. package/dist/esm/getPluginApi-CsTfDB-O.mjs.map +1 -0
  30. package/dist/esm/index.d.mts +1 -1
  31. package/dist/esm/index.mjs +1 -1
  32. package/dist/esm/internals-CCymabFj.mjs.map +1 -1
  33. package/dist/esm/utils.d.mts +70 -2
  34. package/dist/esm/utils.d.mts.map +1 -1
  35. package/dist/esm/utils.mjs +1 -1
  36. package/dist/esm/utils.mjs.map +1 -1
  37. package/dist/esm/validation.d.mts +9 -2
  38. package/dist/esm/validation.d.mts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/Router.ts +20 -0
  41. package/src/api/getPluginApi.ts +33 -2
  42. package/src/internals.ts +12 -0
  43. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +164 -94
  44. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +5 -5
  45. package/src/namespaces/RouterLifecycleNamespace/types.ts +9 -5
  46. package/src/types/RouterValidator.ts +1 -0
  47. package/src/utils/hydrateRouter.ts +33 -0
  48. package/src/utils/index.ts +6 -0
  49. package/src/utils/serializeRouterState.ts +73 -0
  50. package/src/wiring/RouterWiringBuilder.ts +2 -2
  51. package/dist/cjs/Router--UAz5UnF.js +0 -6
  52. package/dist/cjs/Router--UAz5UnF.js.map +0 -1
  53. package/dist/cjs/getPluginApi-BBcZZXA5.js +0 -2
  54. package/dist/cjs/getPluginApi-BBcZZXA5.js.map +0 -1
  55. package/dist/esm/Router-C4grLNrL.mjs +0 -6
  56. package/dist/esm/Router-C4grLNrL.mjs.map +0 -1
  57. package/dist/esm/getPluginApi-DasnID2W.mjs +0 -2
  58. package/dist/esm/getPluginApi-DasnID2W.mjs.map +0 -1
@@ -12,12 +12,25 @@ import type {
12
12
  State,
13
13
  } from "@real-router/types";
14
14
 
15
+ // Cache the assembled PluginApi per router — mirrors getNavigator() (#525):
16
+ // avoids re-allocating the closure-bag on each call (plugins call this once
17
+ // at init, but tests + nested plugins poll it), and gives spy/stub helpers
18
+ // a stable object identity to attach to (e.g. spying on
19
+ // `getPluginApi(router).navigateToState` to inject errors in popstate
20
+ // recovery tests).
21
+ const cache = new WeakMap<object, PluginApi>();
22
+
15
23
  export function getPluginApi<
16
24
  Dependencies extends DefaultDependencies = DefaultDependencies,
17
25
  >(router: Router<Dependencies>): PluginApi {
18
- const ctx = getInternals(router);
26
+ const cached = cache.get(router);
27
+
28
+ if (cached) {
29
+ return cached;
30
+ }
19
31
 
20
- return {
32
+ const ctx = getInternals(router);
33
+ const api: PluginApi = {
21
34
  makeState: (name, params, path, meta) => {
22
35
  ctx.validator?.state.validateMakeStateArgs(name, params, path);
23
36
 
@@ -58,6 +71,20 @@ export function getPluginApi<
58
71
 
59
72
  return ctx.matchPath(path, ctx.getOptions());
60
73
  },
74
+ navigateToState: (state, options) => {
75
+ throwIfDisposed(ctx.isDisposed);
76
+
77
+ ctx.validator?.navigation.validateNavigateToStateArgs(state);
78
+
79
+ if (options !== undefined) {
80
+ ctx.validator?.navigation.validateNavigationOptions(
81
+ options,
82
+ "navigateToState",
83
+ );
84
+ }
85
+
86
+ return ctx.navigateToState(state, options);
87
+ },
61
88
  setRootPath: (rootPath) => {
62
89
  throwIfDisposed(ctx.isDisposed);
63
90
 
@@ -194,4 +221,8 @@ export function getPluginApi<
194
221
  } satisfies ContextNamespaceClaim;
195
222
  }) as PluginApi["claimContextNamespace"],
196
223
  };
224
+
225
+ cache.set(router, api);
226
+
227
+ return api;
197
228
  }
package/src/internals.ts CHANGED
@@ -6,6 +6,7 @@ import type { RouterValidator } from "./types/RouterValidator";
6
6
  import type {
7
7
  DefaultDependencies,
8
8
  EventName,
9
+ NavigationOptions,
9
10
  Options,
10
11
  Params,
11
12
  Plugin,
@@ -55,6 +56,17 @@ export interface RouterInternals<
55
56
 
56
57
  readonly start: (path: string) => Promise<State>;
57
58
 
59
+ /**
60
+ * Plugin-only navigation entry point — delegates to
61
+ * `NavigationNamespace.navigateToState` (`getPluginApi(router).navigateToState`).
62
+ * Hidden from `Router`/`Navigator` to keep the userland surface minimal;
63
+ * see `core-types/src/api.ts` for usage docs.
64
+ */
65
+ readonly navigateToState: (
66
+ state: State,
67
+ options?: NavigationOptions,
68
+ ) => Promise<State>;
69
+
58
70
  /* eslint-disable @typescript-eslint/no-explicit-any -- heterogeneous map: stores different InterceptorFn<M> types under different keys */
59
71
  readonly interceptors: Map<
60
72
  string,
@@ -98,24 +98,176 @@ export class NavigationNamespace {
98
98
  }
99
99
 
100
100
  let toState: State | undefined;
101
- let fromState: State | undefined;
102
- let transitionStarted = false;
103
- let controller: AbortController | null = null;
104
101
 
105
102
  try {
106
103
  toState = deps.buildNavigateState(name, params);
104
+ } catch (error) {
105
+ /* v8 ignore next 3 -- @preserve: reachable only via validator-driven
106
+ throws from buildNavigateState (validateStateBuilderArgs) — covered
107
+ in @real-router/validation-plugin's suite, not in core. */
108
+ return Promise.reject(error as Error);
109
+ }
107
110
 
108
- if (!toState) {
109
- deps.emitTransitionError(
110
- undefined,
111
- deps.getState(),
112
- CACHED_ROUTE_NOT_FOUND_ERROR,
113
- );
114
- this.lastSyncRejected = true;
111
+ if (!toState) {
112
+ deps.emitTransitionError(
113
+ undefined,
114
+ deps.getState(),
115
+ CACHED_ROUTE_NOT_FOUND_ERROR,
116
+ );
117
+ this.lastSyncRejected = true;
115
118
 
116
- return CACHED_ROUTE_NOT_FOUND_REJECTION;
117
- }
119
+ return CACHED_ROUTE_NOT_FOUND_REJECTION;
120
+ }
121
+
122
+ return this.#executeNavigation(toState, opts);
123
+ }
124
+
125
+ /**
126
+ * Navigate to a fully-built `State` directly, skipping `buildNavigateState`
127
+ * (forwardState + buildPath + meta lookup). Used by URL plugins after they
128
+ * have already produced a `State` from a browser-initiated event via
129
+ * `api.matchPath(url)` — see issue #525.
130
+ *
131
+ * Semantics vs. `navigate(name, params, opts)`:
132
+ * - `forwardState` is NOT re-applied. matchPath already runs it; reapplying
133
+ * is redundant in the idempotent case and can race in the dynamic case.
134
+ * - `buildPath` is NOT re-run. The caller's `state.path` is used as-is —
135
+ * so `trailingSlash:"preserve"` matchedState paths flow through unchanged
136
+ * (closes #525 Q2). `buildPath` interceptors do NOT run; the URL the
137
+ * user navigated to is the source of truth for this code path.
138
+ * - All other pipeline steps run unchanged: SAME_STATES check, FSM
139
+ * transition, guards, `subscribeLeave`, `completeTransition`,
140
+ * plugin lifecycle hooks.
141
+ */
142
+ navigateToState(state: State, opts: NavigationOptions): Promise<State> {
143
+ this.lastSyncResolved = false;
144
+ const deps = this.#deps;
145
+
146
+ if (!deps.canNavigate()) {
147
+ this.lastSyncRejected = true;
118
148
 
149
+ return CACHED_NOT_STARTED_REJECTION;
150
+ }
151
+
152
+ // Reject states whose route no longer exists (e.g. the route tree was
153
+ // mutated between matchPath and navigateToState). UNKNOWN_ROUTE is
154
+ // structurally legal — it is the navigateToNotFound output shape.
155
+ if (state.name !== constants.UNKNOWN_ROUTE && !deps.hasRoute(state.name)) {
156
+ const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {
157
+ routeName: state.name,
158
+ });
159
+
160
+ deps.emitTransitionError(undefined, deps.getState(), err);
161
+ this.lastSyncRejected = true;
162
+
163
+ return Promise.reject(err);
164
+ }
165
+
166
+ // States from `matchPath` are deeply frozen (`freezeStateInPlace`).
167
+ // `completeTransition` mutates `toState.transition` and `context` is
168
+ // intentionally extensible for plugin claim writes, so we hand the
169
+ // pipeline a writable shell — same shape `makeState(skipFreeze=true)`
170
+ // produces. `params` stays referentially shared (already frozen).
171
+ // `transition` is omitted so completeTransition can assign it.
172
+ const writableState = {
173
+ name: state.name,
174
+ params: state.params,
175
+ path: state.path,
176
+ context: { ...state.context },
177
+ } as State;
178
+
179
+ return this.#executeNavigation(writableState, opts);
180
+ }
181
+
182
+ navigateToDefault(opts: NavigationOptions): Promise<State> {
183
+ const deps = this.#deps;
184
+ const options = deps.getOptions();
185
+
186
+ if (!options.defaultRoute) {
187
+ return Promise.reject(
188
+ new RouterError(errorCodes.ROUTE_NOT_FOUND, {
189
+ routeName: "defaultRoute not configured",
190
+ }),
191
+ );
192
+ }
193
+
194
+ let route: string;
195
+ let params: Params;
196
+
197
+ try {
198
+ ({ route, params } = deps.resolveDefault());
199
+ } catch (error) {
200
+ return Promise.reject(error as Error);
201
+ }
202
+
203
+ if (!route) {
204
+ return Promise.reject(
205
+ new RouterError(errorCodes.ROUTE_NOT_FOUND, {
206
+ routeName: "defaultRoute resolved to empty",
207
+ }),
208
+ );
209
+ }
210
+
211
+ return this.navigate(route, params, opts);
212
+ }
213
+
214
+ navigateToNotFound(path: string): State {
215
+ this.#abortPreviousNavigation();
216
+
217
+ const fromState = this.#deps.getState();
218
+ const deactivated: string[] = fromState
219
+ ? nameToIDs(fromState.name).toReversed()
220
+ : [];
221
+
222
+ Object.freeze(deactivated);
223
+
224
+ const segments: TransitionMeta["segments"] = {
225
+ deactivated,
226
+ activated: FROZEN_ACTIVATED,
227
+ intersection: "",
228
+ };
229
+
230
+ Object.freeze(segments);
231
+
232
+ const transitionMeta: TransitionMeta = {
233
+ phase: "activating",
234
+ ...(fromState && { from: fromState.name }),
235
+ reason: "success",
236
+ segments,
237
+ };
238
+
239
+ Object.freeze(transitionMeta);
240
+
241
+ const state: State = {
242
+ name: constants.UNKNOWN_ROUTE,
243
+ params: EMPTY_PARAMS as Params,
244
+ path,
245
+ transition: transitionMeta,
246
+ context: {},
247
+ };
248
+
249
+ Object.freeze(state);
250
+
251
+ this.#deps.setState(state);
252
+ this.#deps.emitTransitionSuccess(state, fromState, FROZEN_REPLACE_OPTS);
253
+
254
+ return state;
255
+ }
256
+
257
+ abortCurrentNavigation(): void {
258
+ this.#currentController?.abort(
259
+ new RouterError(errorCodes.TRANSITION_CANCELLED),
260
+ );
261
+ this.#currentController = null;
262
+ }
263
+
264
+ #executeNavigation(toState: State, opts: NavigationOptions): Promise<State> {
265
+ const deps = this.#deps;
266
+ let fromState: State | undefined;
267
+ let transitionStarted = false;
268
+ let controller: AbortController | null = null;
269
+
270
+ try {
119
271
  fromState = deps.getState();
120
272
  opts = forceReplaceFromUnknown(opts, fromState);
121
273
 
@@ -256,88 +408,6 @@ export class NavigationNamespace {
256
408
  }
257
409
  }
258
410
 
259
- navigateToDefault(opts: NavigationOptions): Promise<State> {
260
- const deps = this.#deps;
261
- const options = deps.getOptions();
262
-
263
- if (!options.defaultRoute) {
264
- return Promise.reject(
265
- new RouterError(errorCodes.ROUTE_NOT_FOUND, {
266
- routeName: "defaultRoute not configured",
267
- }),
268
- );
269
- }
270
-
271
- let route: string;
272
- let params: Params;
273
-
274
- try {
275
- ({ route, params } = deps.resolveDefault());
276
- } catch (error) {
277
- return Promise.reject(error as Error);
278
- }
279
-
280
- if (!route) {
281
- return Promise.reject(
282
- new RouterError(errorCodes.ROUTE_NOT_FOUND, {
283
- routeName: "defaultRoute resolved to empty",
284
- }),
285
- );
286
- }
287
-
288
- return this.navigate(route, params, opts);
289
- }
290
-
291
- navigateToNotFound(path: string): State {
292
- this.#abortPreviousNavigation();
293
-
294
- const fromState = this.#deps.getState();
295
- const deactivated: string[] = fromState
296
- ? nameToIDs(fromState.name).toReversed()
297
- : [];
298
-
299
- Object.freeze(deactivated);
300
-
301
- const segments: TransitionMeta["segments"] = {
302
- deactivated,
303
- activated: FROZEN_ACTIVATED,
304
- intersection: "",
305
- };
306
-
307
- Object.freeze(segments);
308
-
309
- const transitionMeta: TransitionMeta = {
310
- phase: "activating",
311
- ...(fromState && { from: fromState.name }),
312
- reason: "success",
313
- segments,
314
- };
315
-
316
- Object.freeze(transitionMeta);
317
-
318
- const state: State = {
319
- name: constants.UNKNOWN_ROUTE,
320
- params: EMPTY_PARAMS as Params,
321
- path,
322
- transition: transitionMeta,
323
- context: {},
324
- };
325
-
326
- Object.freeze(state);
327
-
328
- this.#deps.setState(state);
329
- this.#deps.emitTransitionSuccess(state, fromState, FROZEN_REPLACE_OPTS);
330
-
331
- return state;
332
- }
333
-
334
- abortCurrentNavigation(): void {
335
- this.#currentController?.abort(
336
- new RouterError(errorCodes.TRANSITION_CANCELLED),
337
- );
338
- this.#currentController = null;
339
- }
340
-
341
411
  async #finishAsyncNavigation(
342
412
  guardCompletion: Promise<void>,
343
413
  nav: NavigationContext,
@@ -60,11 +60,11 @@ export class RouterLifecycleNamespace {
60
60
  deps.completeStart();
61
61
 
62
62
  if (matchedState) {
63
- return deps.navigate(
64
- matchedState.name,
65
- matchedState.params,
66
- REPLACE_OPTS,
67
- );
63
+ // navigateToState commits matchedState verbatim — same primitive URL
64
+ // plugins use on popstate / navigate-event (#525). Keeps trailing-slash
65
+ // and any other source-URL flavor that matchPath produced; skips the
66
+ // redundant forwardState+buildPath round-trip in buildNavigateState.
67
+ return deps.navigateToState(matchedState, REPLACE_OPTS);
68
68
  }
69
69
 
70
70
  return deps.navigateToNotFound(startPath);
@@ -9,11 +9,15 @@ import type {
9
9
 
10
10
  export interface RouterLifecycleDependencies {
11
11
  getOptions: () => Options;
12
- navigate: (
13
- name: string,
14
- params: Params,
15
- opts: NavigationOptions,
16
- ) => Promise<State>;
12
+ /**
13
+ * Commit a fully-resolved State without re-running `forwardState`/`buildPath`.
14
+ * `start(path)` uses this to commit `matchPath(path)` directly — the same
15
+ * primitive URL plugins use on popstate / navigate-event (#525). Keeps
16
+ * `state.path` identical to the source URL (preserves trailing slash in
17
+ * `trailingSlash:"preserve"` mode) and avoids the redundant
18
+ * forwardState+buildPath round-trip in `buildNavigateState`.
19
+ */
20
+ navigateToState: (state: State, opts: NavigationOptions) => Promise<State>;
17
21
  navigateToNotFound: (path: string) => State;
18
22
  clearState: () => void;
19
23
  matchPath: <P extends Params = Params>(path: string) => State<P> | undefined;
@@ -124,6 +124,7 @@ export interface RouterValidator {
124
124
  navigation: {
125
125
  validateNavigateArgs: (name: unknown) => void;
126
126
  validateNavigateToDefaultArgs: (options: unknown) => void;
127
+ validateNavigateToStateArgs: (state: unknown) => void;
127
128
  validateNavigationOptions: (options: unknown, caller: string) => void;
128
129
  validateParams: (params: unknown, methodName: string) => void;
129
130
  validateStartArgs: (path: unknown) => void;
@@ -0,0 +1,33 @@
1
+ import type { Router, State } from "@real-router/types";
2
+
3
+ /**
4
+ * Hydrate a fresh router from server-serialized State (#563).
5
+ *
6
+ * Accepts either a JSON string (parsed via `JSON.parse`) or a State-shaped
7
+ * object. Extracts `state.path` and delegates to `router.start(state.path)` —
8
+ * the canonical URL is the source of truth for the router on hydration.
9
+ *
10
+ * The serialized State (produced by `serializeRouterState`) is still useful
11
+ * for application-level concerns: `state.context.<namespace>` payloads (e.g.
12
+ * server-side data from `ssr-data-plugin`) can be read separately by app code
13
+ * before or after `hydrateRouter` resolves.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Client
18
+ * const router = createAppRouter();
19
+ * router.usePlugin(browserPluginFactory());
20
+ * await hydrateRouter(router, window.__SSR_STATE__);
21
+ * ```
22
+ */
23
+ export function hydrateRouter(
24
+ router: Router,
25
+ source: string | { path: string },
26
+ ): Promise<State> {
27
+ const parsed =
28
+ typeof source === "string"
29
+ ? (JSON.parse(source) as { path: string })
30
+ : source;
31
+
32
+ return router.start(parsed.path);
33
+ }
@@ -1,5 +1,11 @@
1
1
  export { getStaticPaths } from "./getStaticPaths";
2
2
 
3
+ export { hydrateRouter } from "./hydrateRouter";
4
+
5
+ export { serializeRouterState } from "./serializeRouterState";
6
+
7
+ export type { SerializeRouterStateOptions } from "./serializeRouterState";
8
+
3
9
  export { serializeState } from "./serializeState";
4
10
 
5
11
  export type { StaticPathEntries } from "./getStaticPaths";
@@ -0,0 +1,73 @@
1
+ import { serializeState } from "./serializeState";
2
+
3
+ import type { State } from "@real-router/types";
4
+
5
+ export interface SerializeRouterStateOptions {
6
+ /**
7
+ * Plugin context namespaces to strip from the serialized output.
8
+ * Use when a plugin populates `state.context.<ns>` with non-JSON-serializable
9
+ * values (e.g., RSC payload: ReactNode trees containing functions/symbols).
10
+ *
11
+ * @default []
12
+ */
13
+ excludeContext?: readonly string[];
14
+ }
15
+
16
+ /**
17
+ * XSS-safe JSON serialization of router State for SSR → client transport (#563).
18
+ *
19
+ * Strips `state.transition` (per-navigation `TransitionMeta` — meaningless after
20
+ * hydration; the client's hydration commit produces its own `transition`).
21
+ * Keeps `name`, `params`, `path`, and `context` (plugin context namespaces are
22
+ * preserved as-is — server's `state.context.data` from `ssr-data-plugin` and
23
+ * any other plugin claims travel to the client untouched).
24
+ *
25
+ * Pass `options.excludeContext` to strip specific namespaces from the output —
26
+ * required for plugins that publish non-JSON-serializable values (e.g., RSC
27
+ * `ReactNode` trees from `@real-router/rsc-server-plugin`).
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Server
32
+ * const state = await router.start(req.url);
33
+ * const html = `<script>window.__SSR_STATE__=${serializeRouterState(state)}</script>`;
34
+ *
35
+ * // Client
36
+ * await hydrateRouter(router, window.__SSR_STATE__);
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // With RSC plugin: strip the "rsc" namespace before transport
42
+ * const state = await router.start(url);
43
+ * const json = serializeRouterState(state, { excludeContext: ["rsc"] });
44
+ * ```
45
+ */
46
+ export function serializeRouterState(
47
+ state: State,
48
+ options?: SerializeRouterStateOptions,
49
+ ): string {
50
+ const exclude = options?.excludeContext;
51
+
52
+ let context = state.context;
53
+
54
+ if (exclude?.length) {
55
+ const filtered: Record<string, unknown> = {};
56
+ const source = state.context as Record<string, unknown>;
57
+
58
+ for (const key of Object.keys(source)) {
59
+ if (!exclude.includes(key)) {
60
+ filtered[key] = source[key];
61
+ }
62
+ }
63
+
64
+ context = filtered;
65
+ }
66
+
67
+ return serializeState({
68
+ name: state.name,
69
+ params: state.params,
70
+ path: state.path,
71
+ context,
72
+ });
73
+ }
@@ -225,8 +225,8 @@ export class RouterWiringBuilder<
225
225
  wireLifecycleDeps(): void {
226
226
  const lifecycleDeps: RouterLifecycleDependencies = {
227
227
  getOptions: () => this.options.get(),
228
- navigate: (name, params, opts) =>
229
- this.navigation.navigate(name, params, opts),
228
+ navigateToState: (state, opts) =>
229
+ this.navigation.navigateToState(state, opts),
230
230
  navigateToNotFound: (path) => this.navigation.navigateToNotFound(path),
231
231
  clearState: () => {
232
232
  this.state.set(undefined);