@nano_kit/router 1.0.0-alpha.4

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/dist/index.js ADDED
@@ -0,0 +1,606 @@
1
+ import { mountable, signal, action, onMount, record, readonly, atIndex, untracked, computed, batch, updateList, inject } from '@nano_kit/store';
2
+
3
+ const PushHistoryAction = "push";
4
+ const ReplaceHistoryAction = "replace";
5
+ const PopHistoryAction = "pop";
6
+
7
+ function onLinkClick(event) {
8
+ const link = event.target.closest("a");
9
+ if (link && event.button === 0 && link.target !== "_blank" && link.origin === location.origin && link.rel !== "external" && link.target !== "_self" && !link.download && !event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.defaultPrevented) {
10
+ event.preventDefault();
11
+ this.push(link.href);
12
+ }
13
+ }
14
+ function listenLinks(navigation) {
15
+ const onClick = onLinkClick.bind(navigation);
16
+ document.body.addEventListener("click", onClick);
17
+ return () => {
18
+ document.body.removeEventListener("click", onClick);
19
+ };
20
+ }
21
+ function resetScroll() {
22
+ window.scrollTo(0, 0);
23
+ }
24
+ function scrollToAnchor(hash, options) {
25
+ if (hash) {
26
+ const id = hash.slice(1);
27
+ const anchor = document.getElementById(id) || document.getElementsByName(id)[0];
28
+ if (anchor) {
29
+ anchor.scrollIntoView(options);
30
+ }
31
+ }
32
+ }
33
+ class ScrollRestorator {
34
+ #storageKeyPrefix;
35
+ /**
36
+ * Create a ScrollRestorator instance.
37
+ * @param storageKeyPrefix - Prefix for session storage keys
38
+ */
39
+ constructor(storageKeyPrefix = "krsp-") {
40
+ this.#storageKeyPrefix = storageKeyPrefix;
41
+ }
42
+ /**
43
+ * Save the current scroll position for the given location.
44
+ * @param location - The location object
45
+ */
46
+ save(location2) {
47
+ sessionStorage.setItem(this.#storageKeyPrefix + location2.href, String(window.scrollY));
48
+ }
49
+ /**
50
+ * Restore the scroll position for the given location.
51
+ * @param location - The location object
52
+ * @returns True if a saved position was restored, false otherwise
53
+ */
54
+ restore(location2) {
55
+ const key = this.#storageKeyPrefix + location2.href;
56
+ const value = parseInt(sessionStorage.getItem(key));
57
+ if (!isNaN(value)) {
58
+ window.scrollTo(0, value);
59
+ this.clear(location2);
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Clear the saved scroll position for the given location.
66
+ * @param location - The location object
67
+ */
68
+ clear(location2) {
69
+ sessionStorage.removeItem(this.#storageKeyPrefix + location2.href);
70
+ }
71
+ }
72
+
73
+ // @__NO_SIDE_EFFECTS__
74
+ function removeTrailingSlash(path) {
75
+ return path.replace(/(.)\/($|\?)/, "$1$2");
76
+ }
77
+ // @__NO_SIDE_EFFECTS__
78
+ function parseHref(href) {
79
+ return new URL(/* @__PURE__ */ removeTrailingSlash(href), "http://a");
80
+ }
81
+ // @__NO_SIDE_EFFECTS__
82
+ function getHref(location) {
83
+ return /* @__PURE__ */ removeTrailingSlash(location.pathname + location.search + location.hash);
84
+ }
85
+ // @__NO_SIDE_EFFECTS__
86
+ function createHrefObject(url) {
87
+ return {
88
+ pathname: url.pathname,
89
+ search: url.search,
90
+ hash: url.hash,
91
+ href: /* @__PURE__ */ getHref(url)
92
+ };
93
+ }
94
+ // @__NO_SIDE_EFFECTS__
95
+ function updateHrefObject(url, update) {
96
+ const updateObject = typeof update === "string" ? /* @__PURE__ */ parseHref(update) : update;
97
+ const updatedUrl = /* @__PURE__ */ parseHref(updateObject.pathname ?? url.pathname);
98
+ updatedUrl.search = updateObject.search ?? url.search;
99
+ updatedUrl.hash = updateObject.hash ?? url.hash;
100
+ if (updateObject.searchParams) {
101
+ updatedUrl.search = updateObject.searchParams.toString();
102
+ }
103
+ return /* @__PURE__ */ createHrefObject(updatedUrl);
104
+ }
105
+ function updateHref(href, update) {
106
+ if (typeof update === "string") {
107
+ return /* @__PURE__ */ removeTrailingSlash(update);
108
+ }
109
+ return (/* @__PURE__ */ updateHrefObject(/* @__PURE__ */ parseHref(href), update)).href;
110
+ }
111
+ // @__NO_SIDE_EFFECTS__
112
+ function composeMatchers(matchers, nomatch = null) {
113
+ const len = matchers.length;
114
+ return (...args) => {
115
+ for (let i = 0, result; i < len; i++) {
116
+ if (result = matchers[i](...args)) {
117
+ return result;
118
+ }
119
+ }
120
+ return nomatch;
121
+ };
122
+ }
123
+ // @__NO_SIDE_EFFECTS__
124
+ function createLocation(url, action = null) {
125
+ return {
126
+ .../* @__PURE__ */ createHrefObject(url),
127
+ action
128
+ };
129
+ }
130
+ // @__NO_SIDE_EFFECTS__
131
+ function updateLocation(url, update, action) {
132
+ return {
133
+ .../* @__PURE__ */ updateHrefObject(url, update),
134
+ action
135
+ };
136
+ }
137
+
138
+ function createPatternRegex(pattern) {
139
+ return new RegExp(`^${removeTrailingSlash(pattern).replace(/[\s!#$()+,.:<=?[\\\]^{|}]/g, "\\$&").replace(/\/\\:(\w+)\\\?/g, "(?:/(?<$1>(?<=/)[^/]+))?").replace(/\/\\:(\w+)/g, "/(?<$1>[^/]+)").replace(/\/\*$/g, "(?:/(?<wildcard>.+))?$")}/?$`, "i");
140
+ }
141
+ function patternMatcher(route, path) {
142
+ const matches = path.match(this);
143
+ if (!matches) {
144
+ return null;
145
+ }
146
+ const params = {};
147
+ if (matches.groups) {
148
+ Object.entries(matches.groups).forEach(([key, value]) => {
149
+ params[key] = value ? decodeURIComponent(value) : "";
150
+ });
151
+ }
152
+ return {
153
+ route,
154
+ params
155
+ };
156
+ }
157
+ const nomatch = {
158
+ route: null,
159
+ params: {}
160
+ };
161
+ function createMatcher$1(routes) {
162
+ return composeMatchers(Object.entries(routes).map(
163
+ ([route, pattern]) => patternMatcher.bind(
164
+ createPatternRegex(pattern),
165
+ route
166
+ )
167
+ ), nomatch);
168
+ }
169
+ const matcherCache$1 = /* @__PURE__ */ new WeakMap();
170
+ function createCachedMatcher$1(routes) {
171
+ let matcher = matcherCache$1.get(routes);
172
+ if (!matcher) {
173
+ matcher = createMatcher$1(routes);
174
+ matcherCache$1.set(routes, matcher);
175
+ }
176
+ return matcher;
177
+ }
178
+ function applyBrowserLocation({ href, action: action2 }) {
179
+ if (action2 === PushHistoryAction) {
180
+ history.pushState(null, "", href);
181
+ } else if (action2 === ReplaceHistoryAction) {
182
+ history.replaceState(null, "", href);
183
+ }
184
+ }
185
+ // @__NO_SIDE_EFFECTS__
186
+ function browserNavigation(routes = {}) {
187
+ const match = createCachedMatcher$1(routes);
188
+ const routerLocation = (location2) => ({
189
+ ...location2,
190
+ ...match(location2.pathname)
191
+ });
192
+ const $location = mountable(signal(
193
+ routerLocation(createLocation(location))
194
+ ));
195
+ const update = (location2) => {
196
+ if (location2 !== null) {
197
+ applyBrowserLocation(location2);
198
+ $location(location2);
199
+ }
200
+ };
201
+ const maybeUpdate = (nextLocation) => {
202
+ const location2 = $location();
203
+ if (location2.href !== nextLocation.href || nextLocation.hash.length > 1) {
204
+ const { action: action2 } = nextLocation;
205
+ const nextRouteLocation = routerLocation(nextLocation);
206
+ if (action2 === null || action2 === PopHistoryAction) {
207
+ update(nextRouteLocation);
208
+ } else {
209
+ navigation.transition(
210
+ update,
211
+ nextRouteLocation,
212
+ location2
213
+ );
214
+ }
215
+ }
216
+ };
217
+ const sync = (event) => {
218
+ maybeUpdate(
219
+ createLocation(
220
+ location,
221
+ event ? PopHistoryAction : null
222
+ )
223
+ );
224
+ };
225
+ const navigation = {
226
+ transition(fn, nextLocation) {
227
+ fn(nextLocation);
228
+ },
229
+ get length() {
230
+ return history.length;
231
+ },
232
+ back: action(() => {
233
+ navigation.transition(history.back, null, $location());
234
+ }),
235
+ forward: action(() => {
236
+ navigation.transition(history.forward, null, $location());
237
+ }),
238
+ push: action((to) => {
239
+ maybeUpdate(
240
+ updateLocation(location, to, PushHistoryAction)
241
+ );
242
+ }),
243
+ replace: action((to) => {
244
+ maybeUpdate(
245
+ updateLocation(location, to, ReplaceHistoryAction)
246
+ );
247
+ })
248
+ };
249
+ onMount($location, () => {
250
+ sync();
251
+ window.addEventListener("popstate", sync);
252
+ return () => {
253
+ window.removeEventListener("popstate", sync);
254
+ };
255
+ });
256
+ return [record(readonly($location)), navigation];
257
+ }
258
+ // @__NO_SIDE_EFFECTS__
259
+ function virtualNavigation(initialPath = "/", routes = {}) {
260
+ const match = createCachedMatcher$1(routes);
261
+ const routerLocation = (location2) => ({
262
+ ...location2,
263
+ ...match(location2.pathname)
264
+ });
265
+ const $history = signal(
266
+ [routerLocation(createLocation(parseHref(initialPath)))]
267
+ );
268
+ const $activeIndex = signal(0);
269
+ const $location = mountable(atIndex($history, $activeIndex));
270
+ const go = (steps) => {
271
+ const newIndex = Math.max(0, Math.min(
272
+ $history().length - 1,
273
+ $activeIndex() + steps
274
+ ));
275
+ batch(() => {
276
+ $activeIndex(newIndex);
277
+ $location((location2) => ({
278
+ ...location2,
279
+ action: PopHistoryAction
280
+ }));
281
+ });
282
+ };
283
+ const back = () => go(-1);
284
+ const forward = () => go(1);
285
+ const update = (location2) => {
286
+ if (location2 !== null) {
287
+ if (location2.action === PushHistoryAction) {
288
+ const activeIndex = $activeIndex();
289
+ const nextIndex = activeIndex + 1;
290
+ batch(() => {
291
+ updateList($history, (history2) => {
292
+ history2.splice(nextIndex, history2.length - activeIndex - 1, location2);
293
+ });
294
+ $activeIndex(nextIndex);
295
+ });
296
+ } else if (location2.action === ReplaceHistoryAction) {
297
+ $location(location2);
298
+ }
299
+ }
300
+ };
301
+ const maybeUpdate = (nextLocation, location2) => {
302
+ if (location2.href !== nextLocation.href || nextLocation.hash.length > 1) {
303
+ const nextRouteLocation = routerLocation(nextLocation);
304
+ navigation.transition(
305
+ update,
306
+ nextRouteLocation,
307
+ location2
308
+ );
309
+ }
310
+ };
311
+ const navigation = {
312
+ transition(fn, location2) {
313
+ fn(location2);
314
+ },
315
+ get length() {
316
+ return untracked($history).length;
317
+ },
318
+ back: action(() => {
319
+ navigation.transition(back, null, $location());
320
+ }),
321
+ forward: action(() => {
322
+ navigation.transition(forward, null, $location());
323
+ }),
324
+ push: action((to) => {
325
+ const location2 = $location();
326
+ maybeUpdate(
327
+ updateLocation(location2, to, PushHistoryAction),
328
+ location2
329
+ );
330
+ }),
331
+ replace: action((to) => {
332
+ const location2 = $location();
333
+ maybeUpdate(
334
+ updateLocation(location2, to, ReplaceHistoryAction),
335
+ location2
336
+ );
337
+ })
338
+ };
339
+ return [record(readonly($location)), navigation];
340
+ }
341
+ // @__NO_SIDE_EFFECTS__
342
+ function routeParam($location, key, parser = (_) => _) {
343
+ const { $params } = $location;
344
+ return computed(() => parser($params()[key]));
345
+ }
346
+
347
+ // @__NO_SIDE_EFFECTS__
348
+ function searchParams($location) {
349
+ const { $search } = $location;
350
+ return computed(() => new URLSearchParams($search()));
351
+ }
352
+ // @__NO_SIDE_EFFECTS__
353
+ function searchParam($searchParams, key, parser = (_) => _) {
354
+ return computed(() => parser($searchParams().get(key)));
355
+ }
356
+
357
+ function execPattern(params) {
358
+ return this.map((part, i) => i % 2 === 0 ? part : part(params)).join("");
359
+ }
360
+ function partToFunction(part, i) {
361
+ return i % 2 === 0 ? part : part === void 0 ? (params = {}) => params.wildcard ? `/${params.wildcard}` : "" : (params = {}) => part in params ? `/${encodeURIComponent(params[part])}` : "";
362
+ }
363
+ function patternToFunction(pattern) {
364
+ const parts = pattern.split(/\/(?::(\w+)\??|\*)/g).map(partToFunction);
365
+ return execPattern.bind(parts);
366
+ }
367
+ // @__NO_SIDE_EFFECTS__
368
+ function buildPaths(routes) {
369
+ return Object.entries(routes).reduce(
370
+ (hrefs, [route, pattern]) => {
371
+ if (/:|\*/.test(pattern)) {
372
+ hrefs[route] = patternToFunction(pattern);
373
+ } else {
374
+ hrefs[route] = removeTrailingSlash(pattern);
375
+ }
376
+ return hrefs;
377
+ },
378
+ {}
379
+ );
380
+ }
381
+ // @__NO_SIDE_EFFECTS__
382
+ function basePath(base, routes) {
383
+ if (!base) {
384
+ return routes;
385
+ }
386
+ const normalizedBase = removeTrailingSlash(base).replace(/^\.\//, "/");
387
+ if (normalizedBase === "" || normalizedBase === "/") {
388
+ return routes;
389
+ }
390
+ return Object.entries(routes).reduce(
391
+ (paths, [route, pattern]) => {
392
+ paths[route] = removeTrailingSlash(`${normalizedBase}${pattern}`);
393
+ return paths;
394
+ },
395
+ {}
396
+ );
397
+ }
398
+
399
+ async function loadPages(pages, tasks) {
400
+ const allTasks = tasks ?? /* @__PURE__ */ new Set();
401
+ for (const ref of pages) {
402
+ if ("expected" in ref) {
403
+ ref.page(allTasks);
404
+ } else {
405
+ ref.layout(allTasks);
406
+ void loadPages(ref.pages, allTasks);
407
+ }
408
+ }
409
+ if (!tasks) {
410
+ await Promise.all(allTasks);
411
+ }
412
+ }
413
+ async function loadPage(pages, route, tasks) {
414
+ const allTasks = tasks ?? /* @__PURE__ */ new Set();
415
+ for (const ref of pages) {
416
+ if ("expected" in ref) {
417
+ if (ref.expected === route) {
418
+ ref.page(allTasks);
419
+ allTasks.add(null);
420
+ break;
421
+ }
422
+ } else if (allTasks.size < (loadPage(ref.pages, route, allTasks), allTasks.size)) {
423
+ ref.layout(allTasks);
424
+ break;
425
+ }
426
+ }
427
+ if (!tasks) {
428
+ await Promise.all(allTasks);
429
+ }
430
+ }
431
+ // @__NO_SIDE_EFFECTS__
432
+ function loadable(load, fallback) {
433
+ const $viewRef = signal({
434
+ view: fallback,
435
+ loading: true
436
+ });
437
+ const init = (tasks) => {
438
+ const promise = load().then((module) => $viewRef({
439
+ view: module.default,
440
+ storesToPreload: module.storesToPreload,
441
+ loading: false
442
+ }));
443
+ tasks?.add(promise);
444
+ };
445
+ let initialized = false;
446
+ return {
447
+ load(tasks) {
448
+ if (!initialized) {
449
+ initialized = true;
450
+ init(tasks);
451
+ }
452
+ return $viewRef();
453
+ }
454
+ };
455
+ }
456
+ function isLoadable(ref) {
457
+ return typeof ref?.load === "function";
458
+ }
459
+ function getViewRefGetter(page2, storesToPreload) {
460
+ let getter;
461
+ if (isLoadable(page2)) {
462
+ getter = page2.load;
463
+ } else {
464
+ const ref = {
465
+ view: page2,
466
+ storesToPreload
467
+ };
468
+ getter = () => ref;
469
+ }
470
+ return getter;
471
+ }
472
+ // @__NO_SIDE_EFFECTS__
473
+ function page(expected, page2, storesToPreload) {
474
+ return {
475
+ expected,
476
+ page: getViewRefGetter(page2, storesToPreload)
477
+ };
478
+ }
479
+ // @__NO_SIDE_EFFECTS__
480
+ function notFound(page2, storesToPreload) {
481
+ return {
482
+ expected: null,
483
+ page: getViewRefGetter(page2, storesToPreload)
484
+ };
485
+ }
486
+ // @__NO_SIDE_EFFECTS__
487
+ function layout(layout2, stroesToPreloadOrPages, maybePages) {
488
+ let pages;
489
+ let storesToPreload;
490
+ if (maybePages === void 0) {
491
+ pages = stroesToPreloadOrPages;
492
+ } else {
493
+ pages = maybePages;
494
+ storesToPreload = stroesToPreloadOrPages;
495
+ }
496
+ return {
497
+ layout: getViewRefGetter(layout2, storesToPreload),
498
+ pages
499
+ };
500
+ }
501
+ function createMatcher(pages) {
502
+ return composeMatchers(pages.map(
503
+ (ref) => "expected" in ref ? createPageMatcher(ref) : createLayoutMatcher(ref)
504
+ ));
505
+ }
506
+ const matcherCache = /* @__PURE__ */ new WeakMap();
507
+ function createCachedMatcher(pages) {
508
+ let matcher = matcherCache.get(pages);
509
+ if (!matcher) {
510
+ matcher = createMatcher(pages);
511
+ matcherCache.set(pages, matcher);
512
+ }
513
+ return matcher;
514
+ }
515
+ function createPageMatcher({ expected, page: page2 }) {
516
+ return (route) => route === expected ? page2() : null;
517
+ }
518
+ function createLayoutMatcher({ layout: layout2, pages }) {
519
+ const match = createCachedMatcher(pages);
520
+ return (route, composed) => {
521
+ const ref = match(route, composed);
522
+ if (ref) {
523
+ const layoutRef = layout2();
524
+ if (layoutRef.loading || !layoutRef.view) {
525
+ return layoutRef;
526
+ }
527
+ const storesToPreload = () => [
528
+ ...layoutRef.storesToPreload?.() ?? [],
529
+ ...ref.storesToPreload?.() ?? []
530
+ ];
531
+ return {
532
+ view: composed(layoutRef.view, ref.view),
533
+ storesToPreload
534
+ };
535
+ }
536
+ return null;
537
+ };
538
+ }
539
+ function createComposed(compose) {
540
+ const storage = /* @__PURE__ */ new Map();
541
+ return (layout2, page2) => {
542
+ let entry = storage.get(layout2);
543
+ if (!entry) {
544
+ const $page = signal(page2);
545
+ entry = {
546
+ page: $page,
547
+ composed: compose?.($page, layout2)
548
+ };
549
+ storage.set(layout2, entry);
550
+ } else {
551
+ entry.page(() => page2);
552
+ }
553
+ return entry.composed;
554
+ };
555
+ }
556
+ // @__NO_SIDE_EFFECTS__
557
+ function router($location, pages, compose) {
558
+ const { $route } = $location;
559
+ const match = createCachedMatcher(pages);
560
+ const composed = createComposed(compose);
561
+ let ref = null;
562
+ const $page = computed(() => (ref = match($route(), composed))?.view ?? null);
563
+ const storesToPreload = () => {
564
+ $page();
565
+ return ref?.storesToPreload?.() ?? [];
566
+ };
567
+ return [$page, storesToPreload];
568
+ }
569
+
570
+ // @__NO_SIDE_EFFECTS__
571
+ function browserNavigation$(routes = {}) {
572
+ const BrowserNavigation$ = () => browserNavigation(routes);
573
+ const Location$ = () => inject(BrowserNavigation$)[0];
574
+ const Navigation$ = () => inject(BrowserNavigation$)[1];
575
+ return [
576
+ Location$,
577
+ Navigation$
578
+ ];
579
+ }
580
+ // @__NO_SIDE_EFFECTS__
581
+ function virtualNavigation$(initialPath, routes) {
582
+ const VirtualNavigation$ = () => virtualNavigation(initialPath, routes);
583
+ const Location$ = () => inject(VirtualNavigation$)[0];
584
+ const Navigation$ = () => inject(VirtualNavigation$)[1];
585
+ return [
586
+ Location$,
587
+ Navigation$
588
+ ];
589
+ }
590
+ // @__NO_SIDE_EFFECTS__
591
+ function router$(Location$, pages, compose) {
592
+ const Router$ = () => router(
593
+ inject(Location$),
594
+ pages,
595
+ compose
596
+ );
597
+ const Page$ = () => inject(Router$)[0];
598
+ const StoresToPreload$ = () => inject(Router$)[1];
599
+ return [
600
+ Page$,
601
+ StoresToPreload$
602
+ ];
603
+ }
604
+
605
+ export { PopHistoryAction, PushHistoryAction, ReplaceHistoryAction, ScrollRestorator, basePath, browserNavigation, browserNavigation$, buildPaths, composeMatchers, createHrefObject, createLocation, getHref, layout, listenLinks, loadPage, loadPages, loadable, notFound, onLinkClick, page, parseHref, removeTrailingSlash, resetScroll, routeParam, router, router$, scrollToAnchor, searchParam, searchParams, updateHref, updateHrefObject, updateLocation, virtualNavigation, virtualNavigation$ };
606
+ //# sourceMappingURL=index.js.map