waymark 0.1.0 → 0.1.1

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.d.ts CHANGED
@@ -1 +1,4 @@
1
- export declare const a = 5;
1
+ export * from "./route";
2
+ export * from "./router";
3
+ export * from "./react";
4
+ export * from "./utils/router";
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
- console.log("Hello via Bun!");
2
- export const a = 5;
1
+ export * from "./route";
2
+ export * from "./router";
3
+ export * from "./react";
4
+ export * from "./utils/router";
@@ -0,0 +1,20 @@
1
+ import { type ReactNode, type AnchorHTMLAttributes, type CSSProperties, type RefAttributes } from "react";
2
+ import { Router, type RouterOptions } from "../router";
3
+ import { type Patterns, type NavigateOptions } from "../utils";
4
+ export type RouterRootProps = RouterOptions | {
5
+ router: Router;
6
+ };
7
+ export declare function RouterRoot(props: RouterRootProps): ReactNode;
8
+ export declare function Outlet(): ReactNode;
9
+ export type NavigateProps<P extends Patterns> = NavigateOptions<P>;
10
+ export declare function Navigate<P extends Patterns>(props: NavigateProps<P>): null;
11
+ export type LinkProps<P extends Patterns> = NavigateOptions<P> & LinkOptions & AnchorHTMLAttributes<HTMLAnchorElement> & RefAttributes<HTMLAnchorElement> & {
12
+ asChild?: boolean;
13
+ };
14
+ export interface LinkOptions {
15
+ preload?: "intent" | "render" | "viewport" | false;
16
+ active?: (currentPath: string, targetPath: string) => boolean;
17
+ activeStyle?: CSSProperties;
18
+ activeClassName?: string;
19
+ }
20
+ export declare function Link<P extends Patterns>(props: LinkProps<P>): ReactNode;
@@ -0,0 +1,107 @@
1
+ import { useMemo, useState, useLayoutEffect, useRef, useEffect, createElement, isValidElement, cloneElement } from "react";
2
+ import { routerContext, outletContext } from "./contexts";
3
+ import { useRouter, useOutlet, _useSubscribe } from "./hooks";
4
+ import { Router } from "../router";
5
+ import { getHref, mergeRefs, defaultLinkActive } from "../utils";
6
+ export function RouterRoot(props) {
7
+ const [router] = useState(() => "router" in props ? props.router : new Router(props));
8
+ const path = _useSubscribe(router, router.history.getPath);
9
+ const route = useMemo(() => router.matchPath(path), [router, path]);
10
+ if (!route) {
11
+ console.error("[Waymark] No route found for path:", path);
12
+ }
13
+ return useMemo(() => {
14
+ return createElement(routerContext.Provider, { value: router }, route?._.components.reduceRight((acc, comp) => createElement(outletContext.Provider, { value: acc }, createElement(comp)), null));
15
+ }, [router, route]);
16
+ }
17
+ // Outlet
18
+ export function Outlet() {
19
+ return useOutlet();
20
+ }
21
+ export function Navigate(props) {
22
+ const router = useRouter();
23
+ useLayoutEffect(() => router.navigate(props), []);
24
+ return null;
25
+ }
26
+ export function Link(props) {
27
+ const ref = useRef(null);
28
+ const router = useRouter();
29
+ const { path, search } = router.composePath(props);
30
+ const currentPath = _useSubscribe(router, router.history.getPath);
31
+ const route = useMemo(() => router.matchPath(path), [router, path]);
32
+ const { to, replace, state, params, search: search_, preload, active, activeStyle, activeClassName, asChild, style, className, children, ...rest } = {
33
+ active: defaultLinkActive,
34
+ ...router.defaultLinkOptions,
35
+ ...props
36
+ };
37
+ const activeProps = useMemo(() => {
38
+ const isActive = active(currentPath, path);
39
+ return {
40
+ ["data-active"]: isActive,
41
+ style: { ...style, ...(isActive && activeStyle) },
42
+ className: [className, isActive && activeClassName].filter(Boolean).join(" ") ||
43
+ undefined
44
+ };
45
+ }, [
46
+ active,
47
+ path,
48
+ currentPath,
49
+ style,
50
+ className,
51
+ activeStyle,
52
+ activeClassName
53
+ ]);
54
+ useEffect(() => {
55
+ if (preload === "render") {
56
+ route?.preload();
57
+ }
58
+ else if (preload === "viewport" && ref.current) {
59
+ const observer = new IntersectionObserver(entries => {
60
+ entries.forEach(entry => {
61
+ if (entry.isIntersecting) {
62
+ route?.preload();
63
+ observer.disconnect();
64
+ }
65
+ });
66
+ });
67
+ observer.observe(ref.current);
68
+ return () => observer.disconnect();
69
+ }
70
+ }, [preload, route]);
71
+ const onClick = (event) => {
72
+ rest.onClick?.(event);
73
+ if (event.ctrlKey ||
74
+ event.metaKey ||
75
+ event.shiftKey ||
76
+ event.altKey ||
77
+ event.button !== 0 ||
78
+ event.defaultPrevented)
79
+ return;
80
+ event.preventDefault();
81
+ router.history.push({ path, search, replace, state });
82
+ };
83
+ const onFocus = (event) => {
84
+ rest.onFocus?.(event);
85
+ if (preload === "intent" && !event.defaultPrevented) {
86
+ route?.preload();
87
+ }
88
+ };
89
+ const onPointerEnter = (event) => {
90
+ rest.onPointerEnter?.(event);
91
+ if (preload === "intent" && !event.defaultPrevented) {
92
+ route?.preload();
93
+ }
94
+ };
95
+ const anchorProps = {
96
+ ...rest,
97
+ ...activeProps,
98
+ ref: mergeRefs(rest.ref, ref),
99
+ href: getHref(path, search),
100
+ onClick,
101
+ onFocus,
102
+ onPointerEnter
103
+ };
104
+ return asChild && isValidElement(children)
105
+ ? cloneElement(children, anchorProps)
106
+ : createElement("a", { ...anchorProps, children });
107
+ }
@@ -0,0 +1,4 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Router } from "../router";
3
+ export declare const routerContext: import("react").Context<Router | undefined>;
4
+ export declare const outletContext: import("react").Context<ReactNode>;
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+ export const routerContext = createContext(undefined);
3
+ export const outletContext = createContext(null);
@@ -0,0 +1,13 @@
1
+ import type { Router } from "../router";
2
+ import type { Routes, RouteSearch, Updater } from "../utils";
3
+ export declare function useRouter(): Router;
4
+ export declare function useOutlet(): import("react").ReactNode;
5
+ export declare function useLocation(): {
6
+ path: string;
7
+ search: URLSearchParams;
8
+ state: any;
9
+ };
10
+ export declare function useNavigate(): <P extends import("..").Patterns>(options: number | import("..").NavigateOptions<P>) => void;
11
+ export declare function useParams<R extends Routes>(route: R): import("..").RouteParams<R>;
12
+ export declare function useSearch<R extends Routes>(route: R): readonly [RouteSearch<R>, (update: Updater<RouteSearch<R>>, replace?: boolean) => void];
13
+ export declare function _useSubscribe<T>(router: Router, getSnapshot: () => T): T;
@@ -0,0 +1,54 @@
1
+ import { useCallback, useContext, useMemo, useSyncExternalStore } from "react";
2
+ import { outletContext, routerContext } from "./contexts";
3
+ // useRouter
4
+ export function useRouter() {
5
+ const router = useContext(routerContext);
6
+ if (!router) {
7
+ throw new Error("[Waymark] useRouter must be used within a router context");
8
+ }
9
+ return router;
10
+ }
11
+ // useOutlet
12
+ export function useOutlet() {
13
+ return useContext(outletContext);
14
+ }
15
+ // useLocation
16
+ export function useLocation() {
17
+ const router = useRouter();
18
+ const path = _useSubscribe(router, router.history.getPath);
19
+ const search = _useSubscribe(router, router.history.getSearch);
20
+ const state = _useSubscribe(router, router.history.getState);
21
+ return useMemo(() => ({ path, search: new URLSearchParams(search), state }), [path, search, state]);
22
+ }
23
+ // useNavigate
24
+ export function useNavigate() {
25
+ const router = useRouter();
26
+ return useMemo(() => router.navigate.bind(router), [router]);
27
+ }
28
+ // useParams
29
+ export function useParams(route) {
30
+ const router = useRouter();
31
+ const path = _useSubscribe(router, router.history.getPath);
32
+ return useMemo(() => router.decomposePath(route, path, router.history.getSearch()).params, [router, route, path]);
33
+ }
34
+ // useSearch
35
+ export function useSearch(route) {
36
+ const router = useRouter();
37
+ const search = _useSubscribe(router, router.history.getSearch);
38
+ const parsed = useMemo(() => router.decomposePath(route, router.history.getPath(), search).search, [router, route, search]);
39
+ const setSearch = useCallback((update, replace) => {
40
+ const { params, search } = router.decomposePath(route, router.history.getPath(), router.history.getSearch());
41
+ update = typeof update === "function" ? update(search) : update;
42
+ router.navigate({
43
+ to: route._.pattern,
44
+ params,
45
+ search: { ...search, ...update },
46
+ replace
47
+ });
48
+ }, [router, route]);
49
+ return [parsed, setSearch];
50
+ }
51
+ // _useSubscribe
52
+ export function _useSubscribe(router, getSnapshot) {
53
+ return useSyncExternalStore(router.history.subscribe, getSnapshot, getSnapshot);
54
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./components";
2
+ export * from "./hooks";
3
+ export * from "./contexts";
@@ -0,0 +1,3 @@
1
+ export * from "./components";
2
+ export * from "./hooks";
3
+ export * from "./contexts";
@@ -0,0 +1,27 @@
1
+ import { type ComponentType } from "react";
2
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
3
+ import { type NormalizePath, type ComponentLoader } from "./utils";
4
+ export declare class Route<P extends string, Ps extends {}, S extends {}> {
5
+ _: {
6
+ pattern: P;
7
+ _params?: Ps;
8
+ _search?: S;
9
+ keys: string[];
10
+ regex: RegExp;
11
+ looseRegex: RegExp;
12
+ mapSearch: (search: Record<string, unknown>) => S;
13
+ components: ComponentType[];
14
+ preloaded: boolean;
15
+ preloaders: (() => Promise<any>)[];
16
+ };
17
+ constructor(pattern: P, mapSearch: (search: Record<string, unknown>) => S, components: ComponentType[], preloaders: (() => Promise<any>)[]);
18
+ route<P2 extends string>(subPattern: P2): Route<NormalizePath<`${P}/${P2}`>, import("regexparam").RouteParams<NormalizePath<`${P}/${P2}`>> extends infer T ? { [KeyType in keyof T]: T[KeyType]; } : never, S>;
19
+ search<S2 extends {}>(mapper: ((search: S & Record<string, unknown>) => S2) | StandardSchemaV1<S & Record<string, unknown>, S2>): Route<P, Ps, (import("type-fest").PickIndexSignature<S> extends infer T_1 ? { [Key in keyof T_1 as Key extends keyof import("type-fest").PickIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K]; } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined; } extends infer T_2 ? { [KeyType_1 in keyof T_2]: T_2[KeyType_1]; } : never> ? never : Key]: T_1[Key]; } : never) & import("type-fest").PickIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K]; } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined; } extends infer T_2 ? { [KeyType_1 in keyof T_2]: T_2[KeyType_1]; } : never> & (import("type-fest").OmitIndexSignature<S> extends infer T_3 ? { [Key_1 in keyof T_3 as Key_1 extends keyof import("type-fest").OmitIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K]; } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined; } extends infer T_4 ? { [KeyType_1 in keyof T_4]: T_4[KeyType_1]; } : never> ? never : Key_1]: T_3[Key_1]; } : never) & import("type-fest").OmitIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K]; } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined; } extends infer T_4 ? { [KeyType_1 in keyof T_4]: T_4[KeyType_1]; } : never> extends infer T ? { [KeyType in keyof T]: T[KeyType]; } : never>;
20
+ component(component: ComponentType): Route<P, Ps, S>;
21
+ lazy(loader: ComponentLoader): Route<P, Ps, S>;
22
+ error(component: ComponentType<{
23
+ error: unknown;
24
+ }>): Route<P, Ps, S>;
25
+ preload(): Promise<void>;
26
+ }
27
+ export declare function route<P extends string>(pattern: P): Route<NormalizePath<P>, import("regexparam").RouteParams<NormalizePath<P>> extends infer T ? { [KeyType in keyof T]: T[KeyType]; } : never, {}>;
package/dist/route.js ADDED
@@ -0,0 +1,57 @@
1
+ import { lazy } from "react";
2
+ import { parse } from "regexparam";
3
+ import { normalizePath, validator, errorBoundary } from "./utils";
4
+ export class Route {
5
+ _;
6
+ constructor(pattern, mapSearch, components, preloaders) {
7
+ const { keys, pattern: regex } = parse(pattern);
8
+ const looseRegex = parse(pattern, true).pattern;
9
+ this._ = {
10
+ pattern,
11
+ keys,
12
+ regex,
13
+ looseRegex,
14
+ mapSearch,
15
+ components,
16
+ preloaded: false,
17
+ preloaders
18
+ };
19
+ }
20
+ route(subPattern) {
21
+ const { pattern, mapSearch, components, preloaders } = this._;
22
+ return new Route(normalizePath(`${pattern}/${subPattern}`), mapSearch, components, preloaders);
23
+ }
24
+ search(mapper) {
25
+ const { pattern, mapSearch, components, preloaders } = this._;
26
+ mapper = validator(mapper);
27
+ return new Route(pattern, search => {
28
+ const mapped = mapSearch(search);
29
+ return { ...mapped, ...mapper(mapped) };
30
+ }, components, preloaders);
31
+ }
32
+ component(component) {
33
+ const { pattern, mapSearch, components, preloaders } = this._;
34
+ return new Route(pattern, mapSearch, [...components, component], preloaders);
35
+ }
36
+ lazy(loader) {
37
+ const { pattern, mapSearch, components, preloaders } = this._;
38
+ const lazyLoader = async () => {
39
+ const result = await loader();
40
+ return "default" in result ? result : { default: result };
41
+ };
42
+ return new Route(pattern, mapSearch, [...components, lazy(lazyLoader)], [...preloaders, loader]);
43
+ }
44
+ error(component) {
45
+ return this.component(errorBoundary(component));
46
+ }
47
+ async preload() {
48
+ const { preloaded, preloaders } = this._;
49
+ if (preloaded)
50
+ return;
51
+ this._.preloaded = true;
52
+ await Promise.all(preloaders.map(loader => loader()));
53
+ }
54
+ }
55
+ export function route(pattern) {
56
+ return new Route(normalizePath(pattern), search => search, [], []);
57
+ }
@@ -0,0 +1,11 @@
1
+ import { type HistoryLike, type HistoryPushOptions } from "../utils";
2
+ export declare class BrowserHistory implements HistoryLike {
3
+ private static patchKey;
4
+ constructor();
5
+ getPath: () => string;
6
+ getSearch: () => string;
7
+ getState: () => any;
8
+ go: (delta: number) => void;
9
+ push: (options: HistoryPushOptions) => void;
10
+ subscribe: (listener: () => void) => () => void;
11
+ }
@@ -0,0 +1,48 @@
1
+ import { getHref, normalizeSearch } from "../utils";
2
+ export class BrowserHistory {
3
+ static patchKey = Symbol.for("waymark_history_patch_v01");
4
+ constructor() {
5
+ if (typeof history !== "undefined" &&
6
+ !Object.hasOwn(window, BrowserHistory.patchKey)) {
7
+ for (const type of [pushStateEvent, replaceStateEvent]) {
8
+ const original = history[type];
9
+ history[type] = function (...args) {
10
+ const result = original.apply(this, args);
11
+ const event = new Event(type);
12
+ event.arguments = args;
13
+ dispatchEvent(event);
14
+ return result;
15
+ };
16
+ }
17
+ Object.assign(window, {
18
+ [BrowserHistory.patchKey]: true
19
+ });
20
+ }
21
+ }
22
+ getPath = () => location.pathname;
23
+ getSearch = () => normalizeSearch(location.search);
24
+ getState = () => history.state;
25
+ go = (delta) => history.go(delta);
26
+ push = (options) => {
27
+ const { path, search, replace, state } = options;
28
+ const href = getHref(path, search);
29
+ history[replace ? replaceStateEvent : pushStateEvent](state, "", href);
30
+ };
31
+ subscribe = (listener) => {
32
+ events.forEach(event => window.addEventListener(event, listener));
33
+ return () => {
34
+ events.forEach(event => window.removeEventListener(event, listener));
35
+ };
36
+ };
37
+ }
38
+ // Events
39
+ const popStateEvent = "popstate";
40
+ const pushStateEvent = "pushState";
41
+ const replaceStateEvent = "replaceState";
42
+ const hashChangeEvent = "hashchange";
43
+ const events = [
44
+ popStateEvent,
45
+ pushStateEvent,
46
+ replaceStateEvent,
47
+ hashChangeEvent
48
+ ];
@@ -0,0 +1,3 @@
1
+ export * from "./router";
2
+ export * from "./browser-history";
3
+ export * from "./memory-history";
@@ -0,0 +1,3 @@
1
+ export * from "./router";
2
+ export * from "./browser-history";
3
+ export * from "./memory-history";
@@ -0,0 +1,18 @@
1
+ import { type HistoryLike, type HistoryPushOptions } from "../utils";
2
+ export interface MemoryLocation {
3
+ path: string;
4
+ search: string;
5
+ state: any;
6
+ }
7
+ export declare class MemoryHistory implements HistoryLike {
8
+ private stack;
9
+ private index;
10
+ private listeners;
11
+ constructor(initial?: Partial<MemoryLocation>);
12
+ getPath: () => string;
13
+ getSearch: () => string;
14
+ getState: () => any;
15
+ go: (delta: number) => void;
16
+ push: (options: HistoryPushOptions) => void;
17
+ subscribe: (listener: () => void) => () => void;
18
+ }
@@ -0,0 +1,39 @@
1
+ import { clamp, normalizeSearch } from "../utils";
2
+ export class MemoryHistory {
3
+ stack = [];
4
+ index = 0;
5
+ listeners = new Set();
6
+ constructor(initial = {}) {
7
+ const { path = "/", search = "", state } = initial;
8
+ this.stack.push({ path, search: normalizeSearch(search), state });
9
+ }
10
+ getPath = () => this.stack[this.index].path;
11
+ getSearch = () => this.stack[this.index].search;
12
+ getState = () => this.stack[this.index].state;
13
+ go = (delta) => {
14
+ this.index = clamp(this.index + delta, 0, this.stack.length - 1);
15
+ this.listeners.forEach(listener => listener());
16
+ };
17
+ push = (options) => {
18
+ const { path, search = "", replace, state } = options;
19
+ const location = {
20
+ path,
21
+ search: normalizeSearch(search),
22
+ state
23
+ };
24
+ this.stack = this.stack.slice(0, this.index + 1);
25
+ if (replace) {
26
+ this.stack[this.index] = location;
27
+ }
28
+ else {
29
+ this.index = this.stack.push(location) - 1;
30
+ }
31
+ this.listeners.forEach(listener => listener());
32
+ };
33
+ subscribe = (listener) => {
34
+ this.listeners.add(listener);
35
+ return () => {
36
+ this.listeners.delete(listener);
37
+ };
38
+ };
39
+ }
@@ -0,0 +1,31 @@
1
+ import type { LinkOptions } from "../react";
2
+ import { type Routes, type PatternRoute, type RouteList, type Patterns, type NavigateOptions, type HistoryLike, type RouteParams, type RouteSearch } from "../utils";
3
+ export interface RouterOptions {
4
+ history?: HistoryLike;
5
+ basePath?: string;
6
+ routes: RouteList;
7
+ defaultLinkOptions?: LinkOptions;
8
+ }
9
+ export declare class Router {
10
+ history: HistoryLike;
11
+ basePath: string;
12
+ routes: RouteList;
13
+ defaultLinkOptions?: LinkOptions;
14
+ _: {
15
+ routeMap: Map<string, Routes>;
16
+ };
17
+ constructor(options: RouterOptions);
18
+ getPath(cpath: string): string;
19
+ getCanonicalPath(path: string): string;
20
+ matchPath(path: string): Routes | undefined;
21
+ getRoute<P extends Patterns>(pattern: P): PatternRoute<P>;
22
+ composePath<P extends Patterns>(options: NavigateOptions<P>): {
23
+ path: string;
24
+ search: string;
25
+ };
26
+ decomposePath<R extends Routes>(route: R, path: string, search: string): {
27
+ params: RouteParams<R>;
28
+ search: RouteSearch<R>;
29
+ };
30
+ navigate<P extends Patterns>(options: NavigateOptions<P> | number): void;
31
+ }
@@ -0,0 +1,64 @@
1
+ import { inject } from "regexparam";
2
+ import { BrowserHistory } from "./browser-history";
3
+ import { normalizePath, extract, stringifySearch, parseSearch } from "../utils";
4
+ export class Router {
5
+ history;
6
+ basePath;
7
+ routes;
8
+ defaultLinkOptions;
9
+ _;
10
+ constructor(options) {
11
+ this.history = options.history ?? new BrowserHistory();
12
+ this.basePath = normalizePath(options.basePath ?? "/");
13
+ this.routes = options.routes;
14
+ this.defaultLinkOptions = options.defaultLinkOptions;
15
+ this._ = {
16
+ routeMap: new Map(options.routes.map(route => [route._.pattern, route]))
17
+ };
18
+ }
19
+ getPath(cpath) {
20
+ return normalizePath(`${this.basePath}/${cpath}`);
21
+ }
22
+ getCanonicalPath(path) {
23
+ if (path === this.basePath || path.startsWith(`${this.basePath}/`)) {
24
+ path = path.slice(this.basePath.length) || "/";
25
+ }
26
+ return path;
27
+ }
28
+ matchPath(path) {
29
+ const cpath = this.getCanonicalPath(path);
30
+ return this.routes.find(route => route._.regex.test(cpath));
31
+ }
32
+ getRoute(pattern) {
33
+ const route = this._.routeMap.get(pattern);
34
+ if (!route) {
35
+ throw new Error(`[Waymark] Route not found for pattern: ${pattern}`);
36
+ }
37
+ return route;
38
+ }
39
+ composePath(options) {
40
+ const { to, params, search } = options;
41
+ return {
42
+ path: this.getPath(params ? inject(to, params) : to),
43
+ search: search ? stringifySearch(search) : ""
44
+ };
45
+ }
46
+ decomposePath(route, path, search) {
47
+ const { keys, looseRegex, mapSearch } = route._;
48
+ const cpath = this.getCanonicalPath(path);
49
+ return {
50
+ params: extract(cpath, looseRegex, keys),
51
+ search: mapSearch(parseSearch(search))
52
+ };
53
+ }
54
+ navigate(options) {
55
+ if (typeof options === "number") {
56
+ this.history.go(options);
57
+ }
58
+ else {
59
+ const { path, search } = this.composePath(options);
60
+ const { replace, state } = options;
61
+ this.history.push({ path, search, replace, state });
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./path";
2
+ export * from "./router";
3
+ export * from "./search";
4
+ export * from "./react";
5
+ export * from "./misc";
@@ -0,0 +1,5 @@
1
+ export * from "./path";
2
+ export * from "./router";
3
+ export * from "./search";
4
+ export * from "./react";
5
+ export * from "./misc";
@@ -0,0 +1,17 @@
1
+ import type { Simplify, EmptyObject } from "type-fest";
2
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
3
+ export type MaybeKey<K extends string, T> = T extends EmptyObject ? {
4
+ [P in K]?: undefined;
5
+ } : {} extends T ? {
6
+ [P in K]?: T;
7
+ } : {
8
+ [P in K]: T;
9
+ };
10
+ export type OptionalOnUndefined<T extends object> = Simplify<{
11
+ [K in keyof T as undefined extends T[K] ? never : K]: T[K];
12
+ } & {
13
+ [K in keyof T as undefined extends T[K] ? K : never]?: T[K];
14
+ }>;
15
+ export declare function getHref(path: string, search?: string): string;
16
+ export declare function clamp(value: number, min: number, max: number): number;
17
+ export declare function validator<Input, Output>(validate: ((input: Input) => Output) | StandardSchemaV1<Input, Output>): (input: Input) => Output;
@@ -0,0 +1,27 @@
1
+ export function getHref(path, search) {
2
+ return `${path}${search ? `?${search}` : ""}`;
3
+ }
4
+ export function clamp(value, min, max) {
5
+ return Math.max(min, Math.min(value, max));
6
+ }
7
+ export function validator(validate) {
8
+ if (typeof validate === "function") {
9
+ return validate;
10
+ }
11
+ else {
12
+ return (input) => {
13
+ const result = validate["~standard"].validate(input);
14
+ if (result instanceof Promise) {
15
+ throw new Error("[Waymark] Validation must be synchronous");
16
+ }
17
+ else if (result.issues) {
18
+ throw new Error("[Waymark] Validation failed", {
19
+ cause: result.issues
20
+ });
21
+ }
22
+ else {
23
+ return result.value;
24
+ }
25
+ };
26
+ }
27
+ }
@@ -0,0 +1,9 @@
1
+ import type { RouteParams } from "regexparam";
2
+ import type { Simplify } from "type-fest";
3
+ export type ParsePattern<P extends string> = Simplify<RouteParams<P>>;
4
+ export type NormalizePath<P extends string> = RemoveTrailingSlash<DedupSlashes<`/${P}`>>;
5
+ type DedupSlashes<P extends string> = P extends `${infer Prefix}//${infer Rest}` ? `${Prefix}${DedupSlashes<`/${Rest}`>}` : P;
6
+ type RemoveTrailingSlash<P extends string> = P extends `${infer Prefix}/` ? Prefix extends "" ? "/" : Prefix : P;
7
+ export declare function normalizePath<P extends string>(path: P): NormalizePath<P>;
8
+ export declare function extract(cpath: string, looseRegex: RegExp, keys: string[]): Record<string, string>;
9
+ export {};
@@ -0,0 +1,19 @@
1
+ export function normalizePath(path) {
2
+ const normalized = `/${path}`
3
+ .replaceAll(/\/+/g, "/")
4
+ .replace(/(.+)\/$/, "$1");
5
+ return normalized;
6
+ }
7
+ export function extract(cpath, looseRegex, keys) {
8
+ const out = {};
9
+ const matches = looseRegex.exec(cpath);
10
+ if (matches) {
11
+ keys.forEach((key, i) => {
12
+ const match = matches[i + 1];
13
+ if (match) {
14
+ out[key] = match;
15
+ }
16
+ });
17
+ }
18
+ return out;
19
+ }
@@ -0,0 +1,10 @@
1
+ import { type Ref, type ComponentType } from "react";
2
+ export type Updater<T extends object> = Partial<T> | ((prev: T) => Partial<T>);
3
+ export type ComponentLoader = () => Promise<ComponentType | {
4
+ default: ComponentType;
5
+ }>;
6
+ export declare function defaultLinkActive(currentPath: string, targetPath: string): boolean;
7
+ export declare function mergeRefs<T>(...inputRefs: (Ref<T> | undefined)[]): Ref<T>;
8
+ export declare function errorBoundary(component: ComponentType<{
9
+ error: unknown;
10
+ }>): ComponentType;
@@ -0,0 +1,50 @@
1
+ import { createElement, Component } from "react";
2
+ import { useOutlet } from "../react";
3
+ export function defaultLinkActive(currentPath, targetPath) {
4
+ return currentPath.startsWith(targetPath);
5
+ }
6
+ export function mergeRefs(...inputRefs) {
7
+ const filtered = inputRefs.filter(r => !!r);
8
+ if (filtered.length <= 1) {
9
+ return filtered[0] ?? null;
10
+ }
11
+ return value => {
12
+ const cleanups = [];
13
+ for (const ref of filtered) {
14
+ const cleanup = assignRef(ref, value);
15
+ cleanups.push(cleanup ?? (() => assignRef(ref, null)));
16
+ }
17
+ return () => cleanups.forEach(cleanup => cleanup());
18
+ };
19
+ }
20
+ function assignRef(ref, value) {
21
+ if (typeof ref === "function") {
22
+ return ref(value);
23
+ }
24
+ else if (ref) {
25
+ ref.current = value;
26
+ }
27
+ }
28
+ export function errorBoundary(component) {
29
+ class Catch extends Component {
30
+ constructor(props) {
31
+ super(props);
32
+ this.state = { children: props.children, error: null };
33
+ }
34
+ static getDerivedStateFromError(error) {
35
+ return { error: [error] };
36
+ }
37
+ static getDerivedStateFromProps(props, state) {
38
+ if (props.children !== state.children) {
39
+ return { children: props.children, error: null };
40
+ }
41
+ return state;
42
+ }
43
+ render() {
44
+ return this.state.error
45
+ ? createElement(component, { error: this.state.error[0] })
46
+ : this.props.children;
47
+ }
48
+ }
49
+ return () => createElement(Catch, { children: useOutlet() });
50
+ }
@@ -0,0 +1,37 @@
1
+ import type { Route } from "../route";
2
+ import type { MaybeKey } from "./misc";
3
+ export interface RegisterRoutes {
4
+ }
5
+ export type RouteList = RegisterRoutes extends {
6
+ routes: infer RouteList extends ReadonlyArray<Route<string, any, any>>;
7
+ } ? RouteList : ReadonlyArray<Route<string, any, any>>;
8
+ export type Routes = RouteList[number];
9
+ export type Patterns = Routes["_"]["pattern"];
10
+ export type RouteParams<R extends Routes> = NonNullable<R["_"]["_params"]>;
11
+ export type RouteSearch<R extends Routes> = NonNullable<R["_"]["_search"]>;
12
+ export type PatternParams<P extends Patterns> = RouteParams<PatternRoute<P>>;
13
+ export type PatternSearch<P extends Patterns> = RouteSearch<PatternRoute<P>>;
14
+ export type PatternRoute<P extends Patterns> = Extract<Routes, {
15
+ _: {
16
+ pattern: P;
17
+ };
18
+ }>;
19
+ export type NavigateOptions<P extends Patterns> = {
20
+ to: P;
21
+ replace?: boolean;
22
+ state?: any;
23
+ } & MaybeKey<"params", PatternParams<P>> & MaybeKey<"search", PatternSearch<P>>;
24
+ export interface HistoryPushOptions {
25
+ path: string;
26
+ search?: string;
27
+ replace?: boolean;
28
+ state?: any;
29
+ }
30
+ export interface HistoryLike {
31
+ getPath: () => string;
32
+ getSearch: () => string;
33
+ getState: () => any;
34
+ go: (delta: number) => void;
35
+ push: (options: HistoryPushOptions) => void;
36
+ subscribe: (listener: () => void) => () => void;
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function normalizeSearch(search: string): string;
2
+ export declare function stringifySearch(search: Record<string, unknown>): string;
3
+ export declare function parseSearch(search: string): Record<string, unknown>;
@@ -0,0 +1,31 @@
1
+ export function normalizeSearch(search) {
2
+ return search.startsWith("?") ? search.slice(1) : search;
3
+ }
4
+ export function stringifySearch(search) {
5
+ return Object.entries(search)
6
+ .filter(([_, value]) => value !== undefined)
7
+ .map(([key, value]) => `${key}=${encodeURIComponent(toValueString(value))}`)
8
+ .join("&");
9
+ }
10
+ export function parseSearch(search) {
11
+ const urlSearch = new URLSearchParams(search);
12
+ return Object.fromEntries([...urlSearch.entries()].map(([key, value]) => {
13
+ value = decodeURIComponent(value);
14
+ return [key, isJSONString(value) ? JSON.parse(value) : value];
15
+ }));
16
+ }
17
+ function toValueString(value) {
18
+ if (typeof value === "string" && !isJSONString(value)) {
19
+ return value;
20
+ }
21
+ return JSON.stringify(value);
22
+ }
23
+ function isJSONString(value) {
24
+ try {
25
+ JSON.parse(value);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waymark",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Simplest strongly typed router for React",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,10 +33,16 @@
33
33
  "prepublishOnly": "bun run build"
34
34
  },
35
35
  "devDependencies": {
36
+ "@standard-schema/spec": "^1.1.0",
36
37
  "@types/bun": "latest",
38
+ "@types/react": "^19.2.8",
39
+ "type-fest": "^5.4.1",
37
40
  "typescript": "^5.9.3"
38
41
  },
39
42
  "peerDependencies": {
40
43
  "react": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "regexparam": "^3.0.0"
41
47
  }
42
48
  }
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- console.log("Hello via Bun!");
2
- export const a = 5;
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": ["ESNext", "DOM"],
5
- "module": "ESNext",
6
- "moduleResolution": "Bundler",
7
- "declaration": true,
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "noUnusedLocals": true,
13
- "noUnusedParameters": true,
14
- "noFallthroughCasesInSwitch": true,
15
- "outDir": "./dist"
16
- },
17
- "include": ["src"],
18
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
19
- }