{"id":23246411,"url":"https://github.com/marianmeres/simple-router","last_synced_at":"2026-02-17T01:35:30.040Z","repository":{"id":63442058,"uuid":"322542402","full_name":"marianmeres/simple-router","owner":"marianmeres","description":"Minimalistic route parser for sapper-like regex routes","archived":false,"fork":false,"pushed_at":"2025-11-30T12:26:31.000Z","size":144,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-02T15:34:18.213Z","etag":null,"topics":["parser","router","routing"],"latest_commit_sha":null,"homepage":"","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/marianmeres.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"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":"2020-12-18T09:01:16.000Z","updated_at":"2025-11-30T12:26:34.000Z","dependencies_parsed_at":"2025-08-22T01:01:27.758Z","dependency_job_id":null,"html_url":"https://github.com/marianmeres/simple-router","commit_stats":{"total_commits":35,"total_committers":3,"mean_commits":"11.666666666666666","dds":0.05714285714285716,"last_synced_commit":"a4bcb4b7e76fbd05062c86acbcf8e88293479f0e"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/marianmeres/simple-router","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marianmeres%2Fsimple-router","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marianmeres%2Fsimple-router/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marianmeres%2Fsimple-router/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marianmeres%2Fsimple-router/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marianmeres","download_url":"https://codeload.github.com/marianmeres/simple-router/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marianmeres%2Fsimple-router/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29529513,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T00:57:22.232Z","status":"ssl_error","status_checked_at":"2026-02-17T00:54:25.811Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["parser","router","routing"],"created_at":"2024-12-19T07:14:51.380Z","updated_at":"2026-02-17T01:35:30.029Z","avatar_url":"https://github.com/marianmeres.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @marianmeres/simple-router\n\n[![NPM version](https://img.shields.io/npm/v/@marianmeres/simple-router)](https://www.npmjs.com/package/@marianmeres/simple-router)\n[![JSR version](https://jsr.io/badges/@marianmeres/simple-router)](https://jsr.io/@marianmeres/simple-router)\n\nA lightweight, framework-agnostic string pattern matcher and router with support for dynamic parameters, wildcards, query strings, and reactive subscriptions.\n\nCan match any string identifiers - URLs, file paths, command names, or custom patterns. Originally inspired by [Sapper-like regex routes](https://sapper.svelte.dev/docs#Regexes_in_routes). Primarily designed for client-side SPA routing, but flexible enough for any pattern matching needs.\n\n## Features\n\n- ✨ Dynamic route parameters with optional regex constraints\n- 🎯 Wildcard and catch-all routes\n- 📦 Spread parameters for multi-segment matching\n- 🔍 Query string parsing (with option to disable per route)\n- 🔄 Reactive subscriptions (Svelte store contract compatible)\n- 🪶 Zero dependencies (except `@marianmeres/pubsub` for subscriptions)\n- 📘 Full TypeScript support\n- 🎨 Framework-agnostic\n\n## Installation\n\n```shell\ndeno add \"jsr:@marianmeres/simple-router\"\n```\n\n```shell\nnpm install @marianmeres/simple-router\n```\n\n```ts\nimport { SimpleRouter } from \"@marianmeres/simple-router\";\n```\n\n## Quick Example\n\n```ts\nimport { SimpleRouter } from \"@marianmeres/simple-router\";\n\n// Routes can be defined via constructor config\nconst router = new SimpleRouter({\n\t\"/\": () =\u003e HomePage,\n\t\"/about\": () =\u003e AboutPage,\n\t\"*\": () =\u003e NotFoundPage, // catch-all fallback\n});\n\n// Or via the \"on\" API\nrouter.on(\"/user/[id([0-9]+)]\", (params) =\u003e {\n\tconsole.log(\"User ID:\", params?.id);\n\treturn UserPage(params?.id);\n});\n\nrouter.on(\"/article/[id]/[slug]\", ({ id, slug }) =\u003e {\n\treturn ArticlePage(id, slug);\n});\n\n// Execute route matching\nconst component = router.exec(\"/user/123\");\n\n// Use with hash routing\nwindow.onhashchange = () =\u003e {\n\tconst component = router.exec(location.hash.slice(1));\n\trender(component);\n};\n```\n\n## Route Patterns\n\n### Basic Segments\n\n- `exact` - Matches exactly \"exact\"\n- `[name]` - Matches any segment, captured as `{ name: \"value\" }`\n- `[name(regex)]` - Matches if regex test passes\n- `[name]?` - Optional segment\n- `[...name]` - Spread params (matches multiple segments)\n- `*` - Wildcard (matches zero or more segments)\n\n### Separators\n\nThe default separator is `/`. Multiple separators are normalized to single, and separators are trimmed from both ends before matching.\n\n## Pattern Examples\n\n| Route Pattern                 | URL Input          | Params Result                    |\n| ----------------------------- | ------------------ | -------------------------------- |\n| `/foo`                        | `/bar`             | `null` (no match)                |\n| `/foo`                        | (empty)            | `null`                           |\n| `/foo/[bar]`                  | `/foo`             | `null`                           |\n| `/`                           | (empty)            | `{}`                             |\n| `foo`                         | `foo`              | `{}`                             |\n| `//foo///bar.baz/`            | `foo/bar.baz`      | `{}`                             |\n| `/[foo]`                      | `/bar`             | `{ foo: \"bar\" }`                 |\n| `#/[foo]/[bar]`               | `#/baz/bat`        | `{ foo: \"baz\", bar: \"bat\" }`     |\n| `/[id([0-9]+)]`               | `/123`             | `{ id: \"123\" }`                  |\n| `/[id([0-9]+)]`               | `/foo`             | `null` (regex fails)             |\n| `/foo/[bar]/[id([0-9]+)]`     | `/foo/baz/123`     | `{ bar: \"baz\", id: \"123\" }`      |\n| `/foo/[bar]?`                 | `/foo`             | `{}`                             |\n| `/foo/[bar]?`                 | `/foo/bar`         | `{ bar: \"bar\" }`                 |\n| `/foo/[bar]?/baz`             | `/foo/bar/baz`     | `{ bar: \"bar\" }`                 |\n| `/[...path]/[file]`           | `/foo/bar/baz.js`  | `{ path: \"foo/bar\", file: \"baz.js\" }` |\n| `/foo/*`                      | `/foo/bar/baz.js`  | `{}`                             |\n| `/[foo]/*`                    | `/foo/bar`         | `{ foo: \"foo\" }`                 |\n\n## API Reference\n\nFor the complete API documentation with all methods, types, and detailed examples, see [API.md](API.md).\n\n### SimpleRouter\n\n#### Constructor\n\n```ts\n// Simple config (backwards compatible)\nconst router = new SimpleRouter({\n\t\"/\": () =\u003e HomePage,\n\t\"/about\": () =\u003e AboutPage\n});\n\n// With options object (for logger support)\nconst router = new SimpleRouter({\n\troutes: {\n\t\t\"/\": () =\u003e HomePage,\n\t\t\"/about\": () =\u003e AboutPage\n\t},\n\tlogger: myLogger // optional, compatible with @marianmeres/clog\n});\n```\n\n- `config` - Either a `RouterConfig` object mapping route patterns to callbacks, or a `RouterOptions` object with `routes` and `logger` properties\n\n#### Methods\n\n##### `on(routes, callback, options?)`\n\nRegister one or more route patterns with a callback.\n\n```ts\nrouter.on(\"/users\", () =\u003e UsersPage);\n\n// Multiple routes to same handler\nrouter.on([\"/\", \"/home\", \"/index.html\"], () =\u003e HomePage);\n\n// With dynamic params\nrouter.on(\"/user/[id]\", (params) =\u003e UserPage(params?.id));\n\n// With regex constraint\nrouter.on(\"/post/[id([0-9]+)]\", (params) =\u003e PostPage(params?.id));\n\n// With label for debugging\nrouter.on(\"/admin\", () =\u003e AdminPage, { label: \"admin-dashboard\" });\n\n// Disable query param parsing for this route\nrouter.on(\"/raw\", (params) =\u003e RawPage, { allowQueryParams: false });\n```\n\n**Options:**\n- `label` - Optional label for debugging (visible via `info()`)\n- `allowQueryParams` - Whether to parse query parameters (default: `true`)\n\n**Important:** Routes are matched in registration order. First match wins!\n\n##### `exec(url, fallbackFn?)`\n\nExecute route matching against a URL.\n\n```ts\nconst result = router.exec(\"/users\");\n\n// With fallback\nrouter.exec(\"/unknown\", () =\u003e console.log(\"Not found\"));\n\n// With query params\nrouter.exec(\"/search?q=hello\");\n```\n\nReturns the value returned by the matched callback, or `false` if no match.\n\n##### `subscribe(callback)`\n\nSubscribe to router state changes. Follows the [Svelte store contract](https://svelte.dev/docs#Store_contract).\n\n```ts\nconst unsubscribe = router.subscribe((state) =\u003e {\n\tconsole.log(\"Route:\", state.route);\n\tconsole.log(\"Params:\", state.params);\n\tconsole.log(\"Label:\", state.label);\n});\n\n// Later\nunsubscribe();\n```\n\nReturns an unsubscribe function directly. The callback is called immediately with the current state, then on every route change.\n\n##### `reset()`\n\nClears all registered routes (except catch-all).\n\n```ts\nrouter.reset().on(\"/new-route\", () =\u003e NewPage);\n```\n\n##### `info()`\n\nReturns a map of registered routes to their labels (for debugging).\n\n```ts\nrouter.on(\"/users\", () =\u003e {}, { label: \"users-list\" });\nconsole.log(router.info()); // { \"/users\": \"users-list\" }\n```\n\n#### Properties\n\n##### `current`\n\nGets the current router state (readonly).\n\n```ts\nrouter.exec(\"/user/123\");\nconsole.log(router.current);\n// { route: \"/user/[id]\", params: { id: \"123\" }, label: null }\n```\n\n##### `static debug`\n\nEnable/disable debug logging. When enabled, uses the logger instance (if provided) or falls back to `console.log`.\n\n```ts\nSimpleRouter.debug = true;\nrouter.exec(\"/test\"); // Logs matching details to console (or custom logger)\n```\n\n### SimpleRoute\n\nLow-level route parser. Usually you don't need to use this directly.\n\n```ts\nimport { SimpleRoute } from \"@marianmeres/simple-router\";\n\nconst route = new SimpleRoute(\"/user/[id([0-9]+)]\");\nconst params = route.parse(\"/user/123\");\nconsole.log(params); // { id: \"123\" }\n```\n\n#### Static Methods\n\n##### `parseQueryString(str)`\n\nParse a query string into an object.\n\n```ts\nSimpleRoute.parseQueryString(\"foo=bar\u0026baz=123\");\n// Returns: { foo: \"bar\", baz: \"123\" }\n```\n\n## TypeScript\n\nFull TypeScript support with generic types for type-safe route callbacks and `exec()` return values.\n\n### Generic Router\n\n`SimpleRouter\u003cT\u003e` is generic over `T`, the return type of route callbacks:\n\n```ts\n// Typed router - all callbacks must return Component, exec() returns Component | false\nconst router = new SimpleRouter\u003cComponent\u003e({\n\t\"/\": () =\u003e HomePage,\n\t\"/about\": () =\u003e AboutPage,\n\t\"*\": () =\u003e NotFoundPage,\n});\n\nconst result = router.exec(\"/about\"); // Component | false\nif (result !== false) {\n\trender(result); // result is Component\n}\n\n// Without explicit type - T is inferred from callbacks (or defaults to unknown)\nconst router2 = new SimpleRouter({\n\t\"/\": () =\u003e \"home\",\n\t\"/about\": () =\u003e \"about\",\n});\n```\n\n### Exported Types\n\n```ts\nimport type {\n\tLogger,\n\tRouteParams,\n\tRouteCallback,     // RouteCallback\u003cT = unknown\u003e\n\tRouterConfig,      // RouterConfig\u003cT = unknown\u003e\n\tRouterCurrent,\n\tRouterOnOptions,\n\tRouterOptions,     // RouterOptions\u003cT = unknown\u003e\n\tRouterSubscriber,\n\tRouterUnsubscribe,\n} from \"@marianmeres/simple-router\";\n```\n\n## Advanced Examples\n\n### SPA with Hash Routing\n\n```ts\n// Type-safe router with Component return type\nconst router = new SimpleRouter\u003cComponent\u003e({\n\t\"/\": () =\u003e HomePage,\n\t\"/about\": () =\u003e AboutPage,\n\t\"/user/[id]\": (params) =\u003e UserPage(params?.id),\n\t\"*\": () =\u003e NotFoundPage,\n});\n\nfunction render(component: Component) {\n\tdocument.getElementById(\"app\").innerHTML = component.render();\n}\n\nwindow.addEventListener(\"hashchange\", () =\u003e {\n\tconst path = location.hash.slice(1) || \"/\";\n\tconst component = router.exec(path); // Component | false\n\tif (component !== false) {\n\t\trender(component);\n\t}\n});\n\n// Trigger initial render\nwindow.dispatchEvent(new HashChangeEvent(\"hashchange\"));\n```\n\n### Route Priority\n\nRoutes are matched in registration order (first match wins):\n\n```ts\nconst router = new SimpleRouter();\n\n// Register generic route first\nrouter.on(\"/user/[id]\", (params) =\u003e {\n\tconsole.log(\"Generic:\", params?.id); // This will match\n});\n\n// Specific route registered second (won't match \"/user/admin\")\nrouter.on(\"/user/admin\", () =\u003e {\n\tconsole.log(\"Admin\"); // This won't be reached\n});\n\nrouter.exec(\"/user/admin\"); // Logs: \"Generic: admin\"\n```\n\nTo fix, register more specific routes first:\n\n```ts\nrouter.on(\"/user/admin\", () =\u003e console.log(\"Admin\"));\nrouter.on(\"/user/[id]\", (params) =\u003e console.log(\"User:\", params?.id));\n```\n\n### Beyond URLs: General Pattern Matching\n\nThe router can match any string patterns, not just URLs:\n\n```ts\n// File path routing\nconst fileRouter = new SimpleRouter({\n\t\"src/[module]/[file].ts\": ({ module, file }) =\u003e\n\t\tconsole.log(`Module: ${module}, File: ${file}`),\n\t\"assets/images/[...path]\": ({ path }) =\u003e\n\t\tconsole.log(`Image path: ${path}`),\n});\n\nfileRouter.exec(\"src/components/Button.ts\");\n// Logs: \"Module: components, File: Button\"\n\nfileRouter.exec(\"assets/images/icons/user.png\");\n// Logs: \"Image path: icons/user.png\"\n\n// Command routing\nconst cmdRouter = new SimpleRouter({\n\t\"user:create\": () =\u003e createUser(),\n\t\"user:delete:[id([0-9]+)]\": ({ id }) =\u003e deleteUser(id),\n\t\"cache:clear:[type(redis|memcached)]?\": ({ type = \"all\" }) =\u003e\n\t\tclearCache(type),\n});\n\ncmdRouter.exec(\"user:delete:123\");\ncmdRouter.exec(\"cache:clear:redis\");\ncmdRouter.exec(\"cache:clear\"); // type defaults to \"all\"\n\n// Custom separator (anything that's not a special regex char works)\nconst dotRouter = new SimpleRouter({\n\t\"app.settings.theme\": () =\u003e \"Theme settings\",\n\t\"app.settings.[section]\": ({ section }) =\u003e `Settings: ${section}`,\n});\n\ndotRouter.exec(\"app.settings.profile\");\n// Returns: \"Settings: profile\"\n```\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarianmeres%2Fsimple-router","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarianmeres%2Fsimple-router","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarianmeres%2Fsimple-router/lists"}