{"id":20838134,"url":"https://github.com/thihathit/rutter","last_synced_at":"2026-03-17T23:01:40.698Z","repository":{"id":143373717,"uuid":"614366275","full_name":"thihathit/rutter","owner":"thihathit","description":"Type-safe framework-agnostic Router, built with URLPattern \u0026 History API.","archived":false,"fork":false,"pushed_at":"2025-05-06T21:17:51.000Z","size":291,"stargazers_count":6,"open_issues_count":6,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-06T21:35:27.651Z","etag":null,"topics":["framework-agnostic","history-api","react","router","solid","svelte","type-safe","typescript","urlpattern","vue"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/rutter","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thihathit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-03-15T12:52:21.000Z","updated_at":"2025-05-06T21:17:31.000Z","dependencies_parsed_at":"2023-04-08T05:33:13.445Z","dependency_job_id":"b002be31-d1e6-4daf-bc1e-cbc86a8db393","html_url":"https://github.com/thihathit/rutter","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thihathit%2Frutter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thihathit%2Frutter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thihathit%2Frutter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thihathit%2Frutter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thihathit","download_url":"https://codeload.github.com/thihathit/rutter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253146115,"owners_count":21861338,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework-agnostic","history-api","react","router","solid","svelte","type-safe","typescript","urlpattern","vue"],"created_at":"2024-11-18T01:09:27.819Z","updated_at":"2026-03-17T23:01:35.653Z","avatar_url":"https://github.com/thihathit.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## About\n\n**Rutter** is a framework-agnostic, lightweight router. Built with [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) \u0026 [History](https://developer.mozilla.org/en-US/docs/Web/API/History_API) API. Internal reactivity is powered by [Signal](https://github.com/preactjs/signals).\n\n\u003e This library doesn't ship polyfill for `URLPattern`. You may consider installing [urlpattern-polyfill](https://www.npmjs.com/package/urlpattern-polyfill).\n\n## Usage\n\n- [\u003cimg src=\"./docs/logos/javascript.svg\" width=\"14\"/\u003e Vanilla JS](#-vanillajs)\n- [\u003cimg src=\"./docs/logos/vue.svg\" width=\"14\"/\u003e Vue](#-vue-bindings-via-shallowrefcomputed)\n- [\u003cimg src=\"./docs/logos/react.svg\" width=\"14\"/\u003e React](#-react-bindings-via-usestatecontext)\n- [\u003cimg src=\"./docs/logos/svelte.svg\" width=\"14\"/\u003e Svelte](#-svelte-bindings-via-readablederived)\n\n### \u003cimg src=\"./docs/logos/javascript.svg\" width=\"14\"/\u003e VanillaJS\n\n```ts\nimport { CreateHistory } from 'rutter'\n\nconst router = new CreateHistory({\n  routes: {\n    index: {\n      pathname: ''\n    },\n    about: {\n      pathname: '/about'\n    },\n    blog: {\n      pathname: '/blog'\n    },\n    blogDetail: {\n      pathname: '/blog/:id'\n    }\n  }\n})\n\nrouter.on('index') // boolean\nrouter.onOneOf(['index', 'about']) // boolean\n```\n\n### \u003cimg src=\"./docs/logos/react.svg\" width=\"14\"/\u003e React bindings: via `useState`/`context`\n\n```tsx\n// router.(tsx|jsx)\n\nimport {\n  FC,\n  PropsWithChildren,\n  createContext,\n  useContext,\n  useEffect,\n  useState\n} from 'react'\n\nimport { CreateHistory } from 'rutter'\n\nexport const {\n  redirect,\n  on,\n  summaryState,\n  routeState,\n  watchSummaryState,\n  watchRouteState\n} = new CreateHistory({\n  routes: {\n    index: {\n      pathname: ''\n    },\n    about: {\n      pathname: '/about'\n    },\n    blog: {\n      pathname: '/blog'\n    },\n    blogDetail: {\n      pathname: '/blog/:id'\n    }\n  }\n})\n\n/**\n * Although using with `context` is recommended for performance reason, you can directly use this hook if you don't want to store all the states in `context` tree.\n */\nexport const useRouterValues = () =\u003e {\n  const [routeStateValue, setRouteStateState] = useState(routeState)\n  const [summaryStateValue, setSummaryStateState] = useState(summaryState)\n\n  useEffect(() =\u003e watchRouteState(setRouteStateState), [])\n  useEffect(() =\u003e watchSummaryState(setSummaryStateState), [])\n\n  return {\n    routeState: routeStateValue,\n    summaryState: summaryStateValue\n  }\n}\n\nconst context = createContext({\n  routeState,\n  summaryState\n})\n\nconst useRouterContext = () =\u003e useContext(context)\n\nexport const RouterProvider: FC\u003cPropsWithChildren\u003e = ({ children }) =\u003e {\n  const value = useRouterValues()\n\n  return \u003ccontext.Provider value={value}\u003e{children}\u003c/context.Provider\u003e\n}\n\nexport const useRoute = () =\u003e {\n  const { routeState } = useRouterContext()\n\n  return routeState\n}\n```\n\n```tsx\n// app.(tsx|jsx)\n\nimport { FC } from 'react'\n\nimport { on, redirect, useRoute, RouterProvider } from './router'\n\nconst Routing: FC = () =\u003e {\n  const { is404, ...restStates } = useRoute()\n\n  return (\n    \u003c\u003e\n      \u003cnav\u003e\n        \u003cbutton onClick={() =\u003e redirect('index')}\u003eIndex\u003c/button\u003e\n\n        \u003cbutton onClick={() =\u003e redirect('blog')}\u003eBlog\u003c/button\u003e\n\n        \u003ca href=\"/invalid-url\"\u003e\n          \u003cbutton\u003e404\u003c/button\u003e\n        \u003c/a\u003e\n      \u003c/nav\u003e\n\n      \u003cfieldset\u003e\n        \u003clegend\u003eBody:\u003c/legend\u003e\n\n        \u003cdiv\u003e\n          {is404 ? (\n            \u003ch1\u003e404 Page\u003c/h1\u003e\n          ) : (\n            \u003c\u003e\n              {on('index') \u0026\u0026 \u003ch1\u003eIndex Page\u003c/h1\u003e}\n\n              {on('about') \u0026\u0026 \u003ch1\u003eAbout Page\u003c/h1\u003e}\n\n              {on('blog') \u0026\u0026 (\n                \u003c\u003e\n                  \u003ch1\u003eBlog Page\u003c/h1\u003e\n\n                  \u003cbutton\n                    onClick={() =\u003e\n                      redirect('blogDetail', {\n                        params: {\n                          id: 123\n                        }\n                      })\n                    }\n                  \u003e\n                    Blog Detail\n                  \u003c/button\u003e\n                \u003c/\u003e\n              )}\n\n              {on('blogDetail') \u0026\u0026 \u003ch1\u003eBlog Detail Page\u003c/h1\u003e}\n            \u003c/\u003e\n          )}\n        \u003c/div\u003e\n      \u003c/fieldset\u003e\n\n      \u003cfieldset\u003e\n        \u003clegend\u003eCurrent route detail:\u003c/legend\u003e\n\n        \u003ccode\u003e\n          \u003cpre\u003e{JSON.stringify(restStates, null, 2)}\u003c/pre\u003e\n        \u003c/code\u003e\n      \u003c/fieldset\u003e\n    \u003c/\u003e\n  )\n}\n\nconst App: FC = () =\u003e (\n  \u003cRouterProvider\u003e\n    \u003cRouting /\u003e\n  \u003c/RouterProvider\u003e\n)\n```\n\n### \u003cimg src=\"./docs/logos/vue.svg\" width=\"14\"/\u003e Vue bindings: via `shallowRef`/`computed`\n\n```ts\n// router.(ts|js)\n\nimport { computed, shallowRef } from 'vue'\nimport { CreateHistory } from 'rutter'\n\nimport { mapValues } from 'lodash-es'\n\nconst router = new CreateHistory({\n  routes: {\n    index: {\n      pathname: ''\n    },\n    about: {\n      pathname: '/about'\n    },\n    blog: {\n      pathname: '/blog'\n    },\n    blogDetail: {\n      pathname: '/blog/:id'\n    }\n  }\n})\n\nconst {\n  //\n  summaryState,\n  routeState,\n  watchSummaryState,\n  watchRouteState,\n  on\n} = router\n\nexport const { redirect } = router\n\nexport const routerState = shallowRef(summaryState)\nexport const route = shallowRef(routeState)\n\nexport const is404 = computed(() =\u003e route.value.is404)\n\nexport const matches = computed(() =\u003e {\n  const { details } = routerState.value\n\n  type RouteNames = keyof typeof details\n\n  return mapValues(details, (_, name) =\u003e on(name as RouteNames))\n})\n\nwatchSummaryState(state =\u003e {\n  routerState.value = state\n})\n\nwatchRouteState(state =\u003e {\n  route.value = state\n})\n```\n\n```vue\n\u003cscript setup lang=\"ts\"\u003e\n// app.vue\nimport { redirect, route, matches, is404 } from './router'\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cnav\u003e\n    \u003cbutton @click=\"() =\u003e redirect('index')\"\u003eIndex\u003c/button\u003e\n\n    \u003cbutton @click=\"() =\u003e redirect('blog')\"\u003eBlog\u003c/button\u003e\n\n    \u003ca href=\"/invalid-url\"\u003e\n      \u003cbutton\u003e404\u003c/button\u003e\n    \u003c/a\u003e\n  \u003c/nav\u003e\n\n  \u003cfieldset\u003e\n    \u003clegend\u003eBody:\u003c/legend\u003e\n    \u003cdiv\u003e\n      \u003ch1 v-if=\"is404\"\u003e404 Page\u003c/h1\u003e\n\n      \u003ctemplate v-else\u003e\n        \u003ch1 v-if=\"matches.index\"\u003eIndex Page\u003c/h1\u003e\n\n        \u003ch1 v-if=\"matches.about\"\u003eAbout Page\u003c/h1\u003e\n\n        \u003ctemplate v-if=\"matches.blog\"\u003e\n          \u003ch1\u003eBlog Page\u003c/h1\u003e\n\n          \u003cbutton\n            @click=\"() =\u003e redirect('blogDetail', { params: { id: 123 } })\"\n          \u003e\n            Blog Detail\n          \u003c/button\u003e\n        \u003c/template\u003e\n\n        \u003ch1 v-if=\"matches.blogDetail\"\u003eBlog Detail Page\u003c/h1\u003e\n      \u003c/template\u003e\n    \u003c/div\u003e\n  \u003c/fieldset\u003e\n\n  \u003cfieldset\u003e\n    \u003clegend\u003eCurrent route detail:\u003c/legend\u003e\n\n    \u003ccode\u003e\n      \u003cpre\u003e{{ route }}\u003c/pre\u003e\n    \u003c/code\u003e\n  \u003c/fieldset\u003e\n\u003c/template\u003e\n```\n\n### \u003cimg src=\"./docs/logos/svelte.svg\" width=\"14\"/\u003e Svelte bindings: via `readable`/`derived`\n\n```ts\n// router.(ts|js)\n\nimport { readable, derived } from 'svelte/store'\nimport { CreateHistory } from 'rutter'\n\nimport { mapValues } from 'lodash-es'\n\nconst router = new CreateHistory({\n  routes: {\n    index: {\n      pathname: ''\n    },\n    about: {\n      pathname: '/about'\n    },\n    blog: {\n      pathname: '/blog'\n    },\n    blogDetail: {\n      pathname: '/blog/:id'\n    }\n  }\n})\n\nconst { summaryState, routeState, watchSummaryState, watchRouteState } = router\n\nexport const { redirect, on, onOneOf } = router\n\nexport const route = readable(routeState, watchRouteState)\nexport const routerState = readable(summaryState, watchSummaryState)\n\nexport const matches = derived(routerState, ({ details }) =\u003e\n  mapValues(details, (_, name) =\u003e on(name as keyof typeof details))\n)\n```\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  // app.svelte\n\n  import { redirect, route, matches } from './router'\n\n  $: ({ is404, ...restState } = $route)\n  $: data = JSON.stringify(restState, null, 2)\n\u003c/script\u003e\n\n\u003cnav\u003e\n  \u003cbutton on:click={() =\u003e redirect('index')}\u003eIndex\u003c/button\u003e\n\n  \u003cbutton on:click={() =\u003e redirect('blog')}\u003eBlog\u003c/button\u003e\n\n  \u003ca href=\"/invalid-url\"\u003e\n    \u003cbutton\u003e404\u003c/button\u003e\n  \u003c/a\u003e\n\u003c/nav\u003e\n\n\u003cfieldset\u003e\n  \u003clegend\u003eBody:\u003c/legend\u003e\n\n  \u003cdiv\u003e\n    {#if is404}\n      \u003ch1\u003e404 Page\u003c/h1\u003e\n    {:else}\n      {#if $matches.index}\n        \u003ch1\u003eIndex Page\u003c/h1\u003e\n      {/if}\n\n      {#if $matches.about}\n        \u003ch1\u003eAbout Page\u003c/h1\u003e\n      {/if}\n\n      {#if $matches.blog}\n        \u003ch1\u003eBlog Page\u003c/h1\u003e\n\n        \u003cbutton\n          on:click={() =\u003e redirect('blogDetail', { params: { id: 123 } })}\n        \u003e\n          Blog Detail\n        \u003c/button\u003e\n      {/if}\n\n      {#if $matches.blogDetail}\n        \u003ch1\u003eBlog Detail Page\u003c/h1\u003e\n      {/if}\n    {/if}\n  \u003c/div\u003e\n\u003c/fieldset\u003e\n\n\u003cfieldset\u003e\n  \u003clegend\u003eCurrent route detail:\u003c/legend\u003e\n\n  \u003ccode\u003e\n    \u003cpre\u003e{data}\u003c/pre\u003e\n  \u003c/code\u003e\n\u003c/fieldset\u003e\n```\n\n## Documentation\n\nType API: https://paka.dev/npm/rutter/api\n\n## Development\n\n```bash\npnpm i\npnpm dev\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthihathit%2Frutter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthihathit%2Frutter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthihathit%2Frutter/lists"}