{"id":16641840,"url":"https://github.com/samthor/rhud","last_synced_at":"2026-04-22T00:33:10.297Z","repository":{"id":42909713,"uuid":"250283093","full_name":"samthor/rhud","owner":"samthor","description":"Router, Henceforth Unlikely Definition","archived":false,"fork":false,"pushed_at":"2022-07-20T23:12:35.000Z","size":278,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-28T16:21:58.191Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/samthor.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}},"created_at":"2020-03-26T14:36:00.000Z","updated_at":"2020-11-14T21:03:23.000Z","dependencies_parsed_at":"2022-09-22T03:30:29.880Z","dependency_job_id":null,"html_url":"https://github.com/samthor/rhud","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/samthor/rhud","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samthor%2Frhud","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samthor%2Frhud/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samthor%2Frhud/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samthor%2Frhud/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/samthor","download_url":"https://codeload.github.com/samthor/rhud/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samthor%2Frhud/sbom","scorecard":{"id":798236,"data":{"date":"2025-08-11","repo":{"name":"github.com/samthor/rhud","commit":"9a9a3b329dd4147d061c83c976d4f5f049b0d5dc"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/main.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/23 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/samthor/rhud/main.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/samthor/rhud/main.yml/master?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 2 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"19 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-2j2x-2gpw-g8fm","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-ww39-953v-wcq6","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-g6ww-v8xp-vmwg","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-4wf5-vphf-c2xc","Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-23T09:38:21.796Z","repository_id":42909713,"created_at":"2025-08-23T09:38:21.796Z","updated_at":"2025-08-23T09:38:21.796Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32115906,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-22T00:31:26.853Z","status":"ssl_error","status_checked_at":"2026-04-22T00:30:22.894Z","response_time":128,"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":[],"created_at":"2024-10-12T07:48:03.672Z","updated_at":"2026-04-22T00:33:10.275Z","avatar_url":"https://github.com/samthor.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Tests](https://github.com/samthor/rhud/workflows/Tests/badge.svg)](https://github.com/samthor/rhud/actions)\n\nModern single-page-application router.\n~3.3kb compiled, 1.5kb gzipped.\nInstalls a global listener for all clicks on `\u003ca href=\"...\"\u003e`.\n\n# Install\n\nInstall via `rhud` using your favorite package manager.\nIncludes TS types.\n\n# Usage\n\nAt its simplest, use like so:\n\n```js\nimport {listen} from 'rhud';  // ESM only\n\nlisten(async (context) =\u003e {\n  const partial = await loadPartialFor(context.href);\n  context.ready(() =\u003e {\n    // If the request was preempted (the user loaded a different page), this\n    // block won't run.\n    updateDom(partial);\n  });\n});\n```\n\nBy default, the router will be triggered for every pathname, not those with \".\" in the URL, except ending with \".html\".\nFor example, \"/foo/bar\" or \"/foo/bar/index.html\" will match, but \"/foo/bar/image.svg\" will not.\n\nIf the function you pass throws an error or returns a rejected `Promise`, the router will just load the page via the browser.\n\n## Options\n\nYou can configure not to be called immediately, and control which URLs are matched:\n\n```js\nlisten((context) =\u003e {\n  // ...\n}, {\n  firstRun: false,  // don't invoke immediately\n  validate: (url) =\u003e url.pathname.exec(/^\\/\\w*$/),  // matches \"/\" or \"/foo\", not \"/x/y\" or \"/foo/\"\n})\n```\n\n## Context\n\nThe listener you configure is passed an object of type `RouterContext`.\nThis contains the requested `href`, as well as:\n\n* `isNavigation`: is this a navigation (e.g., click on link) or is it due to a back/forward history event\n* `firstRun`: is this the first run (`isNavigation` will be false)\n* `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that will be aborted if another route starts before this is complete\n* `state`: the current or future `window.history.state`\n* the `ready()` method: call when you're happy for the URL to change\n\n### Navigation Mode\n\nIn navigation mode, say, the user clicking on a page link, the URL hasn't changed when the listener method is invoked.\nHere's an example:\n\n```js\nlisten(async (context) =\u003e {\n  if (context.isNavigation) {\n    // Here, the URL will still be the page's previous URL. Do long-lived tasks\n    // like network fetches here.\n  }\n\n  // Call .ready() when you're happy for the URL to change.\n  // The method you pass here should be synchronous (can be async if you must).\n  context.ready(() =\u003e {\n    // The URL has changed! Update your DOM synchronously! (This can be\n    // important so relative images work correctly.)\n    // Once this method returns, the page will scroll to any matched hash, e.g.\n    // a link to \"/foo#bar\" will scroll to \"#bar\".\n  });\n\n  // If you have any more pending tasks here (e.g., loading JS for this page)\n  // that can happen after the URL changes, block here.\n});\n```\n\nIf you're _not_ in navigation mode, this means the user has hit back or forward to change the URL.\nIn this case, the URL has already changed when the method opens.\nYou should aim to update the DOM _without_ doing asynchronous calls, as the History API's default behavior is to scroll to the page after a frame—you ideally want your DOM to be ready immediately.\n\n```js\nlisten(async (context) =\u003e {\n  if (context.isNavigation) {\n    // as above, do long-lived tasks\n  } else {\n    // Get partial from cache if possible, but otherwise use network.\n    // Avoid 'await'.\n  }\n\n  context.ready(() =\u003e {\n    // You don't need to do work inside this block outside navigation mode (as\n    // the URL is already up-to-date), but you can still do updates here if it\n    // helps streamline your code.\n  });\n  // as earlier\n});\n```\n\nThere are mitigation strategies if back/forward requires a network request to display properly.\nYou can save the scroll position when the listener runs, reset it after a frame, and then scroll to the intended position later.\n\n### Preempted Router\n\nYou can check whether the router has been preempted (i.e., the user clicked a different link) by checking the `AbortSignal` on `RouterContext`, or call `.maybeAbort()` to just abandon early.\nFor example:\n\n```js\nlisten(async (context) =\u003e {\n  await longLivedTask(context.href);\n  if (context.signal.aborted) {\n    return;  // oh well\n  }\n  context.maybeAbort();  // or: throws an error if preempted\n});\n```\n\n⚠️ If the router has been preempted, then the function passed to `context.ready(() =\u003e ...)` will *not* run.\n\n## Requirements\n\nUses `Promise` (but not `async` or `await`), so probably works on modern browsers.\nThis includes a tiny `AbortSignal` and `AbortController` polyfill (as these are still fairly modern as of 2020), although these won't cancel a `fetch`—check the signal's status when it's important.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamthor%2Frhud","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsamthor%2Frhud","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamthor%2Frhud/lists"}