@octanejs/remix-router 0.1.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/package.json +53 -0
  4. package/src/dom.ts +16 -0
  5. package/src/index.ts +267 -0
  6. package/src/internal.ts +37 -0
  7. package/src/lib/Await.tsrx +145 -0
  8. package/src/lib/Await.tsrx.d.ts +6 -0
  9. package/src/lib/DefaultErrorComponent.tsrx +47 -0
  10. package/src/lib/DefaultErrorComponent.tsrx.d.ts +2 -0
  11. package/src/lib/RenderErrorBoundary.tsrx +117 -0
  12. package/src/lib/RenderErrorBoundary.tsrx.d.ts +14 -0
  13. package/src/lib/actions.ts +90 -0
  14. package/src/lib/components/MemoryRouter.tsrx +42 -0
  15. package/src/lib/components/MemoryRouter.tsrx.d.ts +10 -0
  16. package/src/lib/components/Navigate.ts +60 -0
  17. package/src/lib/components/Router.tsrx +88 -0
  18. package/src/lib/components/Router.tsrx.d.ts +13 -0
  19. package/src/lib/components/RouterProvider.tsrx +320 -0
  20. package/src/lib/components/RouterProvider.tsrx.d.ts +21 -0
  21. package/src/lib/components/Routes.tsrx +53 -0
  22. package/src/lib/components/Routes.tsrx.d.ts +7 -0
  23. package/src/lib/components/routes-collector.ts +317 -0
  24. package/src/lib/components/utils.ts +171 -0
  25. package/src/lib/components/with-props.ts +72 -0
  26. package/src/lib/context.ts +129 -0
  27. package/src/lib/dom/Form.tsrx +76 -0
  28. package/src/lib/dom/Form.tsrx.d.ts +22 -0
  29. package/src/lib/dom/Link.tsrx +91 -0
  30. package/src/lib/dom/Link.tsrx.d.ts +22 -0
  31. package/src/lib/dom/NavLink.tsrx +118 -0
  32. package/src/lib/dom/NavLink.tsrx.d.ts +33 -0
  33. package/src/lib/dom/dom.ts +344 -0
  34. package/src/lib/dom/hooks.ts +93 -0
  35. package/src/lib/dom/lib.ts +822 -0
  36. package/src/lib/dom/routers.tsrx +104 -0
  37. package/src/lib/dom/routers.tsrx.d.ts +23 -0
  38. package/src/lib/dom/server.tsrx +350 -0
  39. package/src/lib/dom/server.tsrx.d.ts +25 -0
  40. package/src/lib/errors.ts +84 -0
  41. package/src/lib/framework-stubs.ts +103 -0
  42. package/src/lib/hooks.ts +1797 -0
  43. package/src/lib/href.ts +71 -0
  44. package/src/lib/react-types.ts +10 -0
  45. package/src/lib/router/history.ts +763 -0
  46. package/src/lib/router/instrumentation.ts +496 -0
  47. package/src/lib/router/links.ts +192 -0
  48. package/src/lib/router/router.ts +7165 -0
  49. package/src/lib/router/server-runtime-types.ts +12 -0
  50. package/src/lib/router/url.ts +8 -0
  51. package/src/lib/router/utils.ts +2179 -0
  52. package/src/lib/server-runtime/cookies.ts +240 -0
  53. package/src/lib/server-runtime/crypto.ts +55 -0
  54. package/src/lib/server-runtime/mode.ts +16 -0
  55. package/src/lib/server-runtime/sessions/cookieStorage.ts +54 -0
  56. package/src/lib/server-runtime/sessions/memoryStorage.ts +59 -0
  57. package/src/lib/server-runtime/sessions.ts +291 -0
  58. package/src/lib/server-runtime/warnings.ts +10 -0
  59. package/src/lib/types/future.ts +16 -0
  60. package/src/lib/types/params.ts +8 -0
  61. package/src/lib/types/register.ts +39 -0
  62. package/src/lib/types/route-module.ts +17 -0
  63. package/src/lib/types/utils.ts +37 -0
@@ -0,0 +1,763 @@
1
+ // Vendored from react-router@7.18.1 packages/react-router/lib/router/history.ts — unmodified.
2
+ // Re-vendor with `node scripts/vendor-remix-router.mjs`; never hand-edit.
3
+ import { PROTOCOL_RELATIVE_URL_REGEX } from './url';
4
+
5
+ ////////////////////////////////////////////////////////////////////////////////
6
+ //#region Types and Constants
7
+ ////////////////////////////////////////////////////////////////////////////////
8
+
9
+ /**
10
+ * Actions represent the type of change to a location value.
11
+ */
12
+ export enum Action {
13
+ /**
14
+ * A POP indicates a change to an arbitrary index in the history stack, such
15
+ * as a back or forward navigation. It does not describe the direction of the
16
+ * navigation, only that the current index changed.
17
+ *
18
+ * Note: This is the default action for newly created history objects.
19
+ */
20
+ Pop = 'POP',
21
+
22
+ /**
23
+ * A PUSH indicates a new entry being added to the history stack, such as when
24
+ * a link is clicked and a new page loads. When this happens, all subsequent
25
+ * entries in the stack are lost.
26
+ */
27
+ Push = 'PUSH',
28
+
29
+ /**
30
+ * A REPLACE indicates the entry at the current index in the history stack
31
+ * being replaced by a new one.
32
+ */
33
+ Replace = 'REPLACE',
34
+ }
35
+
36
+ /**
37
+ * The pathname, search, and hash values of a URL.
38
+ */
39
+ export interface Path {
40
+ /**
41
+ * A URL pathname, beginning with a /.
42
+ */
43
+ pathname: string;
44
+
45
+ /**
46
+ * A URL search string, beginning with a ?.
47
+ */
48
+ search: string;
49
+
50
+ /**
51
+ * A URL fragment identifier, beginning with a #.
52
+ */
53
+ hash: string;
54
+ }
55
+
56
+ // TODO: (v7) Change the Location generic default from `any` to `unknown` and
57
+ // remove Remix `useLocation` wrapper.
58
+
59
+ /**
60
+ * An entry in a history stack. A location contains information about the
61
+ * URL path, as well as possibly some arbitrary state and a key.
62
+ */
63
+ export interface Location<State = any> extends Path {
64
+ /**
65
+ * A value of arbitrary data associated with this location.
66
+ */
67
+ state: State;
68
+
69
+ /**
70
+ * A unique string associated with this location. May be used to safely store
71
+ * and retrieve data in some other storage API, like `localStorage`.
72
+ *
73
+ * Note: This value is always "default" on the initial location.
74
+ */
75
+ key: string;
76
+
77
+ /**
78
+ * The masked location displayed in the URL bar, which differs from the URL the
79
+ * router is operating on
80
+ */
81
+ mask?: Path;
82
+ }
83
+
84
+ /**
85
+ * A change to the current location.
86
+ */
87
+ export interface Update {
88
+ /**
89
+ * The action that triggered the change.
90
+ */
91
+ action: Action;
92
+
93
+ /**
94
+ * The new location.
95
+ */
96
+ location: Location;
97
+
98
+ /**
99
+ * The delta between this location and the former location in the history stack
100
+ */
101
+ delta: number | null;
102
+ }
103
+
104
+ /**
105
+ * A function that receives notifications about location changes.
106
+ */
107
+ export interface Listener {
108
+ (update: Update): void;
109
+ }
110
+
111
+ /**
112
+ * Describes a location that is the destination of some navigation used in
113
+ * {@link Link}, {@link useNavigate}, etc.
114
+ */
115
+ export type To = string | Partial<Path>;
116
+
117
+ /**
118
+ * A history is an interface to the navigation stack. The history serves as the
119
+ * source of truth for the current location, as well as provides a set of
120
+ * methods that may be used to change it.
121
+ *
122
+ * It is similar to the DOM's `window.history` object, but with a smaller, more
123
+ * focused API.
124
+ */
125
+ export interface History {
126
+ /**
127
+ * The last action that modified the current location. This will always be
128
+ * Action.Pop when a history instance is first created. This value is mutable.
129
+ */
130
+ readonly action: Action;
131
+
132
+ /**
133
+ * The current location. This value is mutable.
134
+ */
135
+ readonly location: Location;
136
+
137
+ /**
138
+ * Returns a valid href for the given `to` value that may be used as
139
+ * the value of an <a href> attribute.
140
+ *
141
+ * @param to - The destination URL
142
+ */
143
+ createHref(to: To): string;
144
+
145
+ /**
146
+ * Returns a URL for the given `to` value
147
+ *
148
+ * @param to - The destination URL
149
+ */
150
+ createURL(to: To): URL;
151
+
152
+ /**
153
+ * Encode a location the same way window.history would do (no-op for memory
154
+ * history) so we ensure our PUSH/REPLACE navigations for data routers
155
+ * behave the same as POP
156
+ *
157
+ * @param to Unencoded path
158
+ */
159
+ encodeLocation(to: To): Path;
160
+
161
+ /**
162
+ * Pushes a new location onto the history stack, increasing its length by one.
163
+ * If there were any entries in the stack after the current one, they are
164
+ * lost.
165
+ *
166
+ * @param to - The new URL
167
+ * @param state - Data to associate with the new location
168
+ */
169
+ push(to: To, state?: any): void;
170
+
171
+ /**
172
+ * Replaces the current location in the history stack with a new one. The
173
+ * location that was replaced will no longer be available.
174
+ *
175
+ * @param to - The new URL
176
+ * @param state - Data to associate with the new location
177
+ */
178
+ replace(to: To, state?: any): void;
179
+
180
+ /**
181
+ * Navigates `n` entries backward/forward in the history stack relative to the
182
+ * current index. For example, a "back" navigation would use go(-1).
183
+ *
184
+ * @param delta - The delta in the stack index
185
+ */
186
+ go(delta: number): void;
187
+
188
+ /**
189
+ * Sets up a listener that will be called whenever the current location
190
+ * changes.
191
+ *
192
+ * @param listener - A function that will be called when the location changes
193
+ * @returns unlisten - A function that may be used to stop listening
194
+ */
195
+ listen(listener: Listener): () => void;
196
+ }
197
+
198
+ type HistoryState = {
199
+ usr: any;
200
+ key?: string;
201
+ idx: number;
202
+ masked?: Path;
203
+ };
204
+
205
+ const PopStateEventType = 'popstate';
206
+
207
+ function isLocation(obj: unknown): obj is Location {
208
+ return (
209
+ typeof obj === 'object' &&
210
+ obj != null &&
211
+ 'pathname' in obj &&
212
+ 'search' in obj &&
213
+ 'hash' in obj &&
214
+ 'state' in obj &&
215
+ 'key' in obj
216
+ );
217
+ }
218
+
219
+ //#endregion
220
+
221
+ ////////////////////////////////////////////////////////////////////////////////
222
+ //#region Memory History
223
+ ////////////////////////////////////////////////////////////////////////////////
224
+
225
+ /**
226
+ * A user-supplied object that describes a location. Used when providing
227
+ * entries to `createMemoryHistory` via its `initialEntries` option.
228
+ */
229
+ export type InitialEntry = string | Partial<Location>;
230
+
231
+ export type MemoryHistoryOptions = {
232
+ initialEntries?: InitialEntry[];
233
+ initialIndex?: number;
234
+ v5Compat?: boolean;
235
+ };
236
+
237
+ /**
238
+ * A memory history stores locations in memory. This is useful in stateful
239
+ * environments where there is no web browser, such as node tests or React
240
+ * Native.
241
+ */
242
+ export interface MemoryHistory extends History {
243
+ /**
244
+ * The current index in the history stack.
245
+ */
246
+ readonly index: number;
247
+ }
248
+
249
+ /**
250
+ * Memory history stores the current location in memory. It is designed for use
251
+ * in stateful non-browser environments like tests and React Native.
252
+ */
253
+ export function createMemoryHistory(options: MemoryHistoryOptions = {}): MemoryHistory {
254
+ let { initialEntries = ['/'], initialIndex, v5Compat = false } = options;
255
+ let entries: Location[]; // Declare so we can access from createMemoryLocation
256
+ entries = initialEntries.map((entry, index) =>
257
+ createMemoryLocation(
258
+ entry,
259
+ typeof entry === 'string' ? null : entry.state,
260
+ index === 0 ? 'default' : undefined,
261
+ typeof entry === 'string' ? undefined : entry.mask,
262
+ ),
263
+ );
264
+ let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);
265
+ let action = Action.Pop;
266
+ let listener: Listener | null = null;
267
+
268
+ function clampIndex(n: number): number {
269
+ return Math.min(Math.max(n, 0), entries.length - 1);
270
+ }
271
+ function getCurrentLocation(): Location {
272
+ return entries[index];
273
+ }
274
+ function createMemoryLocation(to: To, state: any = null, key?: string, mask?: Path): Location {
275
+ let location = createLocation(
276
+ entries ? getCurrentLocation().pathname : '/',
277
+ to,
278
+ state,
279
+ key,
280
+ mask,
281
+ );
282
+ warning(
283
+ location.pathname.charAt(0) === '/',
284
+ `relative pathnames are not supported in memory history: ${JSON.stringify(to)}`,
285
+ );
286
+ return location;
287
+ }
288
+
289
+ function createHref(to: To) {
290
+ return typeof to === 'string' ? to : createPath(to);
291
+ }
292
+
293
+ let history: MemoryHistory = {
294
+ get index() {
295
+ return index;
296
+ },
297
+ get action() {
298
+ return action;
299
+ },
300
+ get location() {
301
+ return getCurrentLocation();
302
+ },
303
+ createHref,
304
+ createURL(to) {
305
+ return new URL(createHref(to), 'http://localhost');
306
+ },
307
+ encodeLocation(to: To) {
308
+ let path = typeof to === 'string' ? parsePath(to) : to;
309
+ return {
310
+ pathname: path.pathname || '',
311
+ search: path.search || '',
312
+ hash: path.hash || '',
313
+ };
314
+ },
315
+ push(to, state) {
316
+ action = Action.Push;
317
+ let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state);
318
+ index += 1;
319
+ entries.splice(index, entries.length, nextLocation);
320
+ if (v5Compat && listener) {
321
+ listener({ action, location: nextLocation, delta: 1 });
322
+ }
323
+ },
324
+ replace(to, state) {
325
+ action = Action.Replace;
326
+ let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state);
327
+ entries[index] = nextLocation;
328
+ if (v5Compat && listener) {
329
+ listener({ action, location: nextLocation, delta: 0 });
330
+ }
331
+ },
332
+ go(delta) {
333
+ action = Action.Pop;
334
+ let nextIndex = clampIndex(index + delta);
335
+ let nextLocation = entries[nextIndex];
336
+ index = nextIndex;
337
+ if (listener) {
338
+ listener({ action, location: nextLocation, delta });
339
+ }
340
+ },
341
+ listen(fn: Listener) {
342
+ listener = fn;
343
+ return () => {
344
+ listener = null;
345
+ };
346
+ },
347
+ };
348
+
349
+ return history;
350
+ }
351
+ //#endregion
352
+
353
+ ////////////////////////////////////////////////////////////////////////////////
354
+ //#region Browser History
355
+ ////////////////////////////////////////////////////////////////////////////////
356
+
357
+ /**
358
+ * A browser history stores the current location in regular URLs in a web
359
+ * browser environment. This is the standard for most web apps and provides the
360
+ * cleanest URLs the browser's address bar.
361
+ *
362
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory
363
+ */
364
+ export interface BrowserHistory extends UrlHistory {}
365
+
366
+ export type BrowserHistoryOptions = UrlHistoryOptions;
367
+
368
+ /**
369
+ * Browser history stores the location in regular URLs. This is the standard for
370
+ * most web apps, but it requires some configuration on the server to ensure you
371
+ * serve the same app at multiple URLs.
372
+ *
373
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
374
+ */
375
+ export function createBrowserHistory(options: BrowserHistoryOptions = {}): BrowserHistory {
376
+ function createBrowserLocation(window: Window, globalHistory: Window['history']) {
377
+ let maskedLocation = (globalHistory.state as HistoryState)?.masked;
378
+ let { pathname, search, hash } = maskedLocation || window.location;
379
+ return createLocation(
380
+ '',
381
+ { pathname, search, hash },
382
+ // state defaults to `null` because `window.history.state` does
383
+ (globalHistory.state && globalHistory.state.usr) || null,
384
+ (globalHistory.state && globalHistory.state.key) || 'default',
385
+ maskedLocation
386
+ ? {
387
+ pathname: window.location.pathname,
388
+ search: window.location.search,
389
+ hash: window.location.hash,
390
+ }
391
+ : undefined,
392
+ );
393
+ }
394
+
395
+ function createBrowserHref(window: Window, to: To) {
396
+ return typeof to === 'string' ? to : createPath(to);
397
+ }
398
+
399
+ return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
400
+ }
401
+ //#endregion
402
+
403
+ ////////////////////////////////////////////////////////////////////////////////
404
+ //#region Hash History
405
+ ////////////////////////////////////////////////////////////////////////////////
406
+
407
+ /**
408
+ * A hash history stores the current location in the fragment identifier portion
409
+ * of the URL in a web browser environment.
410
+ *
411
+ * This is ideal for apps that do not control the server for some reason
412
+ * (because the fragment identifier is never sent to the server), including some
413
+ * shared hosting environments that do not provide fine-grained controls over
414
+ * which pages are served at which URLs.
415
+ *
416
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory
417
+ */
418
+ export interface HashHistory extends UrlHistory {}
419
+
420
+ export type HashHistoryOptions = UrlHistoryOptions;
421
+
422
+ /**
423
+ * Hash history stores the location in window.location.hash. This makes it ideal
424
+ * for situations where you don't want to send the location to the server for
425
+ * some reason, either because you do cannot configure it or the URL space is
426
+ * reserved for something else.
427
+ *
428
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
429
+ */
430
+ export function createHashHistory(options: HashHistoryOptions = {}): HashHistory {
431
+ function createHashLocation(window: Window, globalHistory: Window['history']) {
432
+ let { pathname = '/', search = '', hash = '' } = parsePath(window.location.hash.substring(1));
433
+
434
+ // Hash URL should always have a leading / just like window.location.pathname
435
+ // does, so if an app ends up at a route like /#something then we add a
436
+ // leading slash so all of our path-matching behaves the same as if it would
437
+ // in a browser router. This is particularly important when there exists a
438
+ // root splat route (<Route path="*">) since that matches internally against
439
+ // "/*" and we'd expect /#something to 404 in a hash router app.
440
+ if (!pathname.startsWith('/') && !pathname.startsWith('.')) {
441
+ pathname = '/' + pathname;
442
+ }
443
+
444
+ return createLocation(
445
+ '',
446
+ { pathname, search, hash },
447
+ // state defaults to `null` because `window.history.state` does
448
+ (globalHistory.state && globalHistory.state.usr) || null,
449
+ (globalHistory.state && globalHistory.state.key) || 'default',
450
+ );
451
+ }
452
+
453
+ function createHashHref(window: Window, to: To) {
454
+ let base = window.document.querySelector('base');
455
+ let href = '';
456
+
457
+ if (base && base.getAttribute('href')) {
458
+ let url = window.location.href;
459
+ let hashIndex = url.indexOf('#');
460
+ href = hashIndex === -1 ? url : url.slice(0, hashIndex);
461
+ }
462
+
463
+ return href + '#' + (typeof to === 'string' ? to : createPath(to));
464
+ }
465
+
466
+ function validateHashLocation(location: Location, to: To) {
467
+ warning(
468
+ location.pathname.charAt(0) === '/',
469
+ `relative pathnames are not supported in hash history.push(${JSON.stringify(to)})`,
470
+ );
471
+ }
472
+
473
+ return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);
474
+ }
475
+ //#endregion
476
+
477
+ ////////////////////////////////////////////////////////////////////////////////
478
+ //#region UTILS
479
+ ////////////////////////////////////////////////////////////////////////////////
480
+
481
+ /**
482
+ * @private
483
+ */
484
+ export function invariant(value: boolean, message?: string): asserts value;
485
+ export function invariant<T>(value: T | null | undefined, message?: string): asserts value is T;
486
+ export function invariant(value: any, message?: string) {
487
+ if (value === false || value === null || typeof value === 'undefined') {
488
+ throw new Error(message);
489
+ }
490
+ }
491
+
492
+ export function warning(cond: any, message: string) {
493
+ if (!cond) {
494
+ if (typeof console !== 'undefined') console.warn(message);
495
+
496
+ try {
497
+ // Welcome to debugging history!
498
+ //
499
+ // This error is thrown as a convenience, so you can more easily
500
+ // find the source for a warning that appears in the console by
501
+ // enabling "pause on exceptions" in your JavaScript debugger.
502
+ throw new Error(message);
503
+ } catch (
504
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
505
+ e
506
+ ) {}
507
+ }
508
+ }
509
+
510
+ function createKey() {
511
+ return Math.random().toString(36).substring(2, 10);
512
+ }
513
+
514
+ /**
515
+ * For browser-based histories, we combine the state and key into an object
516
+ */
517
+ function getHistoryState(location: Location, index: number): HistoryState {
518
+ return {
519
+ usr: location.state,
520
+ key: location.key,
521
+ idx: index,
522
+ masked: location.mask
523
+ ? {
524
+ pathname: location.pathname,
525
+ search: location.search,
526
+ hash: location.hash,
527
+ }
528
+ : undefined,
529
+ };
530
+ }
531
+
532
+ /**
533
+ * Creates a Location object with a unique key from the given Path
534
+ */
535
+ export function createLocation(
536
+ current: string | Location,
537
+ to: To,
538
+ state: any = null,
539
+ key?: string,
540
+ mask?: Path,
541
+ ): Readonly<Location> {
542
+ let location: Readonly<Location> = {
543
+ pathname: typeof current === 'string' ? current : current.pathname,
544
+ search: '',
545
+ hash: '',
546
+ ...(typeof to === 'string' ? parsePath(to) : to),
547
+ state,
548
+ // TODO: This could be cleaned up. push/replace should probably just take
549
+ // full Locations now and avoid the need to run through this flow at all
550
+ // But that's a pretty big refactor to the current test suite so going to
551
+ // keep as is for the time being and just let any incoming keys take precedence
552
+ key: (to && (to as Location).key) || key || createKey(),
553
+ mask,
554
+ };
555
+ return location;
556
+ }
557
+
558
+ /**
559
+ * Creates a string URL path from the given pathname, search, and hash components.
560
+ *
561
+ * @category Utils
562
+ */
563
+ export function createPath({ pathname = '/', search = '', hash = '' }: Partial<Path>) {
564
+ if (search && search !== '?') pathname += search.charAt(0) === '?' ? search : '?' + search;
565
+ if (hash && hash !== '#') pathname += hash.charAt(0) === '#' ? hash : '#' + hash;
566
+ return pathname;
567
+ }
568
+
569
+ /**
570
+ * Parses a string URL path into its separate pathname, search, and hash components.
571
+ *
572
+ * @category Utils
573
+ */
574
+ export function parsePath(path: string): Partial<Path> {
575
+ let parsedPath: Partial<Path> = {};
576
+
577
+ if (path) {
578
+ let hashIndex = path.indexOf('#');
579
+ if (hashIndex >= 0) {
580
+ parsedPath.hash = path.substring(hashIndex);
581
+ path = path.substring(0, hashIndex);
582
+ }
583
+
584
+ let searchIndex = path.indexOf('?');
585
+ if (searchIndex >= 0) {
586
+ parsedPath.search = path.substring(searchIndex);
587
+ path = path.substring(0, searchIndex);
588
+ }
589
+
590
+ if (path) {
591
+ parsedPath.pathname = path;
592
+ }
593
+ }
594
+
595
+ return parsedPath;
596
+ }
597
+
598
+ export interface UrlHistory extends History {}
599
+
600
+ export type UrlHistoryOptions = {
601
+ window?: Window;
602
+ v5Compat?: boolean;
603
+ };
604
+
605
+ function getUrlBasedHistory(
606
+ getLocation: (window: Window, globalHistory: Window['history']) => Location,
607
+ createHref: (window: Window, to: To) => string,
608
+ validateLocation: ((location: Location, to: To) => void) | null,
609
+ options: UrlHistoryOptions = {},
610
+ ): UrlHistory {
611
+ let { window = document.defaultView!, v5Compat = false } = options;
612
+ let globalHistory = window.history;
613
+ let action = Action.Pop;
614
+ let listener: Listener | null = null;
615
+
616
+ let index = getIndex()!;
617
+ // Index should only be null when we initialize. If not, it's because the
618
+ // user called history.pushState or history.replaceState directly, in which
619
+ // case we should log a warning as it will result in bugs.
620
+ if (index == null) {
621
+ index = 0;
622
+ globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
623
+ }
624
+
625
+ function getIndex(): number {
626
+ let state = globalHistory.state || { idx: null };
627
+ return state.idx;
628
+ }
629
+
630
+ function handlePop() {
631
+ action = Action.Pop;
632
+ let nextIndex = getIndex();
633
+ let delta = nextIndex == null ? null : nextIndex - index;
634
+ index = nextIndex;
635
+ if (listener) {
636
+ listener({ action, location: history.location, delta });
637
+ }
638
+ }
639
+
640
+ function push(to: Location | To, state?: any) {
641
+ action = Action.Push;
642
+ let location = isLocation(to) ? to : createLocation(history.location, to, state);
643
+ if (validateLocation) validateLocation(location, to);
644
+
645
+ index = getIndex() + 1;
646
+ let historyState = getHistoryState(location, index);
647
+ let url = history.createHref(location.mask || location);
648
+
649
+ // try...catch because iOS limits us to 100 pushState calls :/
650
+ try {
651
+ globalHistory.pushState(historyState, '', url);
652
+ } catch (error) {
653
+ // If the exception is because `state` can't be serialized, let that throw
654
+ // outwards just like a replace call would so the dev knows the cause
655
+ // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
656
+ // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
657
+ if (error instanceof DOMException && error.name === 'DataCloneError') {
658
+ throw error;
659
+ }
660
+ // They are going to lose state here, but there is no real
661
+ // way to warn them about it since the page will refresh...
662
+ window.location.assign(url);
663
+ }
664
+
665
+ if (v5Compat && listener) {
666
+ listener({ action, location: history.location, delta: 1 });
667
+ }
668
+ }
669
+
670
+ function replace(to: To, state?: any) {
671
+ action = Action.Replace;
672
+ let location = isLocation(to) ? to : createLocation(history.location, to, state);
673
+ if (validateLocation) validateLocation(location, to);
674
+
675
+ index = getIndex();
676
+ let historyState = getHistoryState(location, index);
677
+ let url = history.createHref(location.mask || location);
678
+ globalHistory.replaceState(historyState, '', url);
679
+
680
+ if (v5Compat && listener) {
681
+ listener({ action, location: history.location, delta: 0 });
682
+ }
683
+ }
684
+
685
+ function createURL(to: To): URL {
686
+ return createBrowserURLImpl(window, to);
687
+ }
688
+
689
+ let history: History = {
690
+ get action() {
691
+ return action;
692
+ },
693
+ get location() {
694
+ return getLocation(window, globalHistory);
695
+ },
696
+ listen(fn: Listener) {
697
+ if (listener) {
698
+ throw new Error('A history only accepts one active listener');
699
+ }
700
+ window.addEventListener(PopStateEventType, handlePop);
701
+ listener = fn;
702
+
703
+ return () => {
704
+ window.removeEventListener(PopStateEventType, handlePop);
705
+ listener = null;
706
+ };
707
+ },
708
+ createHref(to) {
709
+ return createHref(window, to);
710
+ },
711
+ createURL,
712
+ encodeLocation(to) {
713
+ // Encode a Location the same way window.location would
714
+ let url = createURL(to);
715
+ return {
716
+ pathname: url.pathname,
717
+ search: url.search,
718
+ hash: url.hash,
719
+ };
720
+ },
721
+ push,
722
+ replace,
723
+ go(n) {
724
+ return globalHistory.go(n);
725
+ },
726
+ };
727
+
728
+ return history;
729
+ }
730
+
731
+ export function createBrowserURLImpl(windowImpl: Window, to: To, isAbsolute = false): URL {
732
+ let base = 'http://localhost';
733
+ if (windowImpl) {
734
+ // window.location.origin is "null" (the literal string value) in Firefox
735
+ // under certain conditions, notably when serving from a local HTML file
736
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
737
+ base =
738
+ windowImpl.location.origin !== 'null' ? windowImpl.location.origin : windowImpl.location.href;
739
+ }
740
+
741
+ invariant(base, 'No window.location.(origin|href) available to create URL');
742
+
743
+ let href = typeof to === 'string' ? to : createPath(to);
744
+
745
+ // Treating this as a full URL will strip any trailing spaces so we need to
746
+ // pre-encode them since they might be part of a matching splat param from
747
+ // an ancestor route
748
+ href = href.replace(/ $/, '%20');
749
+
750
+ // If this isn't a usage for absolute URLs (currently only for redirects),
751
+ // then we need to avoid the URL constructor treating a leading double slash
752
+ // as a protocol-less URL. By prepending the base, it forces the double slash
753
+ // to be parsed correctly as part of the pathname.
754
+ if (!isAbsolute && PROTOCOL_RELATIVE_URL_REGEX.test(href)) {
755
+ // new URL('//', 'https://localhost') -> error!
756
+ // new URL('https://localhost//', 'https://localhost') -> no error!
757
+ href = base + href;
758
+ }
759
+
760
+ return new URL(href, base);
761
+ }
762
+
763
+ //#endregion