rutter
Version:
Framework-agnostic, lightweight router.
1 lines • 14.5 kB
Source Map (JSON)
{"version":3,"sources":["../core/router.ts","../utility/index.ts","../utility/functions.ts","../core/normalizers.ts","../utility/url.ts"],"names":["signal","computed","effect","utility_exports","__export","buildURL","mapValues","original","iteratee","values","newValues","key","endsWith","string","target","position","length","end","functions_default","trailingSlash","pathname","content","baseURL","excludedQueryParams","options","params","queryParams","hash","path","v","tokenName","tokenValue","url","name","value","getCurrentURL","CreateHistory","#Pattern","#url","#routeData","#withPattern","search","normalize","rest","patternOptions","#details","routeData","#current","details","route","#route","info","detail","is404","#summary","#events","#watchers","routes","URLPattern","#autoUpdate","#getWatcherCleaner","watcher","action","currentRN","isMatch","names","replace","pattern","URL","callback","stop"],"mappings":"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","sourcesContent":["import { Signal, signal, computed, effect } from '@preact/signals-core'\n\nimport { mapValues, buildURL } from '$utility'\n\nimport {\n RouteName,\n Data,\n RedirectMode,\n MainRedirectOptions,\n MetaValue\n} from './types'\nimport { trailingSlash } from './normalizers'\n\ntype Options<RN extends RouteName, FieldsMeta extends MetaValue> = {\n /** Register your routes here. */\n routes: Data<RN, FieldsMeta>\n\n /** Replace with ponyfill. */\n URLPattern?: unknown\n}\n\nconst getCurrentURL = () => new URL(self.location.toString())\n\nexport class CreateHistory<RN extends RouteName, FieldsMeta = MetaValue> {\n #Pattern = URLPattern\n\n #url = signal(getCurrentURL())\n\n /** `Reactive` */\n #routeData: Signal<Data<RN, FieldsMeta>>\n\n /** `Reactive` */\n #withPattern = computed(() =>\n mapValues(\n this.#routeData.value,\n ({ pathname, hash, search, normalize = true, ...rest }) => {\n const patternOptions = { hash, search, pathname }\n\n if (normalize) {\n if (!trailingSlash.matchAny(pathname)) {\n patternOptions.pathname = `${pathname}{/}?`\n }\n }\n\n const pattern = new this.#Pattern(patternOptions)\n\n return {\n pattern,\n normalize,\n ...rest\n }\n }\n )\n )\n\n /** `Reactive` */\n #details = computed(() =>\n mapValues(this.#withPattern.value, routeData => ({\n ...routeData,\n\n /** `Matched against current route` */\n isMatch: routeData.pattern.test(this.#url.value),\n\n /** `Matched against current route. In details` */\n detail: routeData.pattern.exec(this.#url.value)\n }))\n )\n\n /** `Reactive` */\n #current = computed(() => {\n const details = this.#details.value\n\n const routeNames = Object.keys(details) as RN[]\n\n const route = routeNames\n .map(name => ({\n name,\n route: details[name]\n }))\n .filter(({ route }) => !route.ignore)\n .find(({ route }) => route.isMatch)\n\n return route?.name\n })\n\n /** `Reactive` */\n #route = computed(() => {\n const url = this.#url.value\n const info = this.getCurrentDetail()\n\n const detail = info?.detail\n const name = info?.name\n\n const is404 = info === undefined\n const params = {\n ...detail?.pathname.groups\n }\n const hash = url.hash\n\n const search = new URLSearchParams(url.search)\n const queryParams = Object.fromEntries(search)\n\n return {\n name,\n is404,\n params,\n queryParams,\n hash,\n info\n }\n })\n\n /** `Reactive` */\n #summary = computed(() => ({\n url: this.#url.value,\n routeData: this.#routeData.value,\n withPattern: this.#withPattern.value,\n details: this.#details.value,\n route: this.#route.value,\n current: this.#current.value\n }))\n\n /** Registered events. Invoked upon cleanup process */\n #events: VoidFunction[] = []\n\n /** Registered effects. Invoked upon cleanup process */\n #watchers: VoidFunction[] = []\n\n /** History API based router. */\n constructor({ routes, URLPattern }: Options<RN, FieldsMeta>) {\n // @ts-ignore\n if (URLPattern) this.#Pattern = URLPattern\n\n this.#routeData = signal(routes)\n\n this.#events.push(this.#autoUpdate())\n }\n\n #getWatcherCleaner = (watcher: VoidFunction) => {\n return () => {\n watcher()\n\n this.#watchers = this.#watchers.filter(effect => effect !== watcher)\n }\n }\n\n /** Update upon URL change */\n #autoUpdate = () => {\n const action = () => {\n this.update()\n }\n\n self.addEventListener('popstate', action)\n\n // Produce cleanup code\n return () => {\n self.removeEventListener('popstate', action)\n }\n }\n\n /** Overall detail */\n get summaryState() {\n return this.#summary.value\n }\n\n /** Current route detail */\n get routeState() {\n return this.#route.value\n }\n\n getDetail = (name: RN) => {\n return this.#details.value[name]\n }\n\n getCurrentDetail = () => {\n const name = this.#current.value\n\n if (!name) return\n\n return { name, ...this.getDetail(name) }\n }\n\n /** Refresh route information */\n update = () => {\n this.#url.value = getCurrentURL()\n }\n\n /** Check current route name */\n on = (name: RN) => {\n const currentRN = this.#current.value\n\n if (!currentRN) return false\n\n const { isMatch } = this.getDetail(currentRN)\n\n return isMatch && currentRN === name\n }\n\n /**\n * Same as `on()` but accepts multiple route names.\n *\n * Returns `true` if one of them matches.\n */\n onOneOf = (names: RN[]) => {\n const matches = names.map(name => this.on(name))\n\n return !!matches.filter(Boolean).length\n }\n\n /**\n * Similar to `on()` except this only check for route pattern.\n *\n * Whereas `on` consider options such as `ignore`.\n */\n onRouteMatch = (name: RN) => {\n return this.getDetail(name).isMatch\n }\n\n /** Jump between routes. */\n redirect = (name: RN, options: MainRedirectOptions = {}) => {\n const { hash, params, queryParams } = this.#route.value\n\n const { replace = false, ...rest } = (() => {\n if (typeof options == 'function') {\n return options({\n hash,\n params,\n queryParams\n })\n }\n\n return options\n })()\n\n const { pattern } = this.getDetail(name)\n\n const URL = buildURL(pattern.pathname, rest)\n\n const method: RedirectMode = replace ? 'replaceState' : 'pushState'\n\n history[method](null, '', URL)\n\n this.update()\n }\n\n /** Watch: `summaryState` */\n watchSummaryState = (\n callback: (\n summaryState: CreateHistory<RN, FieldsMeta>['summaryState']\n ) => void\n ) => {\n const watcher = effect(() => callback(this.#summary.value))\n\n this.#watchers.push(watcher)\n\n return this.#getWatcherCleaner(watcher)\n }\n\n /** Watch: `routeState` */\n watchRouteState = (\n callback: (routeState: CreateHistory<RN, FieldsMeta>['routeState']) => void\n ) => {\n const watcher = effect(() => callback(this.#route.value))\n\n this.#watchers.push(watcher)\n\n return this.#getWatcherCleaner(watcher)\n }\n\n /** De-register events, watchers */\n destroy = () => {\n this.#watchers.forEach(stop => stop())\n this.#events.forEach(stop => stop())\n\n this.#events = []\n this.#watchers = []\n }\n}\n\nexport default CreateHistory\n","export * from './url'\nexport * from './functions'\n","export const mapValues = <Value extends object, Result extends object>(\n original: Value,\n iteratee: (\n value: Value[keyof Value],\n key: keyof Value,\n original: Value\n ) => Result\n) => {\n type Key = keyof Value\n type NewValues = Record<Key, Result>\n\n const values = Object(original)\n const newValues = {} as NewValues\n\n Object.keys(values).forEach(key => {\n newValues[key as Key] = iteratee(values[key], key as Key, values)\n })\n\n return newValues\n}\n\n/**\n * Checks if `string` ends with the given target string.\n */\nconst endsWith = (string: string, target: string, position?: number) => {\n const { length } = string\n position = position === undefined ? length : +position\n if (position < 0 || position !== position) {\n position = 0\n } else if (position > length) {\n position = length\n }\n const end = position\n position -= target.length\n return position >= 0 && string.slice(position, end) === target\n}\n\nexport default endsWith\n","import endsWith from '$utility/functions'\n\nexport const trailingSlash = {\n pattern: '{/}?',\n matchers: ['/', '{/}', '{/}?'],\n matchAny(pathname: string) {\n return !!this.matchers.filter(content => endsWith(pathname, content)).length\n }\n}\n","import { trailingSlash } from '$core/normalizers'\n\ntype Value = string | number | bigint | boolean | undefined\ntype Data = {\n [key: string]: Value\n}\nconst baseURL = self.location.origin\nconst excludedQueryParams: Value[] = [undefined, '']\n\nexport type URLBuilderOptions = {\n params?: Data\n hash?: string\n queryParams?: Data\n}\n\nexport const buildURL = (\n pathname: string,\n options?: URLBuilderOptions\n): URL => {\n const { params = {}, queryParams = {}, hash } = { ...options }\n\n // Exclude normalized pattern\n if (trailingSlash.matchAny(pathname)) {\n pathname = pathname.split(trailingSlash.pattern)[0]\n }\n\n const path = pathname\n .split('/')\n .map(v => {\n const tokenName = v.replace(':', '')\n const tokenValue = params[tokenName]\n\n if (tokenValue && v.startsWith(':')) {\n return v.replace(`:${tokenName}`, `${tokenValue}`)\n }\n\n return v\n })\n .join('/')\n\n const url = new URL(path, baseURL)\n\n for (const [name, value] of Object.entries(queryParams)) {\n if (excludedQueryParams.includes(value)) continue\n\n url.searchParams.set(name, `${value}`)\n }\n\n if (hash) {\n url.hash = hash\n }\n\n return url\n}\n"]}