{"id":28523129,"url":"https://github.com/philipahlberg/runway","last_synced_at":"2026-05-19T07:31:57.075Z","repository":{"id":33223993,"uuid":"122475633","full_name":"philipahlberg/runway","owner":"philipahlberg","description":"A modern router for web apps based on web components","archived":false,"fork":false,"pushed_at":"2023-03-31T10:29:42.000Z","size":1176,"stargazers_count":0,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-08T19:28:20.315Z","etag":null,"topics":["router","typescript","web-components"],"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/philipahlberg.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}},"created_at":"2018-02-22T12:28:59.000Z","updated_at":"2023-11-07T12:48:00.000Z","dependencies_parsed_at":"2024-06-21T13:03:03.057Z","dependency_job_id":"b9c93c0a-7e91-4a2e-81ac-7248e438c020","html_url":"https://github.com/philipahlberg/runway","commit_stats":{"total_commits":209,"total_committers":3,"mean_commits":69.66666666666667,"dds":0.416267942583732,"last_synced_commit":"63788727d74edfafed79edfe69c7fed97d07af99"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/philipahlberg/runway","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philipahlberg%2Frunway","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philipahlberg%2Frunway/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philipahlberg%2Frunway/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philipahlberg%2Frunway/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/philipahlberg","download_url":"https://codeload.github.com/philipahlberg/runway/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philipahlberg%2Frunway/sbom","scorecard":{"id":731292,"data":{"date":"2025-08-11","repo":{"name":"github.com/philipahlberg/runway","commit":"b527508fd2cb33b5173cc0462c6b85a0c2a8c9cc"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"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":"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/dispatch_publish.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/dispatch_publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/dispatch_publish.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/dispatch_publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/on_push.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/on_push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/on_push.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/on_push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/on_push.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/on_push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/on_push.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/on_push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/on_tag.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/on_tag.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/on_tag.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/philipahlberg/runway/on_tag.yml/main?enable=pin","Warn: npmCommand not pinned by hash: .github/workflows/dispatch_publish.yml:15","Warn: npmCommand not pinned by hash: .github/workflows/on_push.yml:18","Warn: npmCommand not pinned by hash: .github/workflows/on_push.yml:28","Warn: npmCommand not pinned by hash: .github/workflows/on_tag.yml:17","Info:   0 out of   8 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   4 npmCommand 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":"Code-Review","score":0,"reason":"Found 0/30 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":"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":"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":"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":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/dispatch_publish.yml:1","Warn: no topLevel permission defined: .github/workflows/on_push.yml:1","Warn: no topLevel permission defined: .github/workflows/on_tag.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":"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: MIT License: 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 'main'"],"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":"Vulnerabilities","score":0,"reason":"18 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-r7qp-cfhv-p84w","Warn: Project is vulnerable to: GHSA-q9mw-68c2-j6m5","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-76p7-773f-r4q5","Warn: Project is vulnerable to: GHSA-25hc-qcg6-38wj","Warn: Project is vulnerable to: GHSA-qm95-pgcg-qqfq","Warn: Project is vulnerable to: GHSA-cqmj-92xf-r6r9","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-fhg7-m89q-25r3","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q"],"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-22T14:30:32.512Z","repository_id":33223993,"created_at":"2025-08-22T14:30:32.512Z","updated_at":"2025-08-22T14:30:32.512Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33206312,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-19T07:16:55.748Z","status":"ssl_error","status_checked_at":"2026-05-19T07:16:54.366Z","response_time":58,"last_error":"SSL_read: 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":["router","typescript","web-components"],"created_at":"2025-06-09T10:07:19.860Z","updated_at":"2026-05-19T07:31:57.057Z","avatar_url":"https://github.com/philipahlberg.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Runway\nA modern, framework-agnostic router for building single-page applications with web components.\n\n## Warning\nRunway has not reached 1.0 yet, and so the API is unstable. Expect breaking changes between minor versions until 1.0.\n\n## Overview\nRunway is heavily inspired by the Vue router, so if you've used that before, you should feel right at home with Runway.\n\nGetting started with Runway is fairly simple. At its core, Runway is a mapping of URL paths to components; you declare a path, and the component you want to be rendered when the path is visited:\n```js\nimport Router from 'runway';\n\n// 1. Import your component(s)\nimport Component from './my-component.js';\n\n// 2. Define your routes\nconst routes = [\n  {\n    path: 'foo',\n    component: Component\n  }\n];\n\n// 3. Create the router\nconst router = new Router({\n  // all paths will be prefixed with `/`\n  root: '/',\n  routes,\n});\n\n// 4. Connect the router to an element\nrouter.connect(document.body);\n```\nTo visit `'/foo'`, you can:\n\n  1) Call `router.push('/foo')` or `router.replace('/foo')`\n  2) Click on a `\u003crouter-link to=\"/foo\"\u003e` element\n\nIf you need your route to match a pattern instead of a path, you can use Express-like named parameters in the path:\n```js\nconst router = new Router({\n  root: '/',\n  routes: [\n    {\n      path: ':param',\n      component: Component\n    }\n  ],\n});\n```\n\nWhen using a named parameter, you can access the value by utilizing the `properties` function option:\n```js\nconst router = new Router({\n  root: '/',\n  routes: [\n    {\n      path: ':param',\n      component: Component,\n      properties: ({ parameters }) =\u003e ({\n        myProp: parameters.get('param')\n      }),\n    },\n  ],\n});\n```\nIn this example, when an instance of `Component` is created, the property `myProp` is set on the instance with the value of the `param` parameter.\nIf the user visited `/foo`, the value of `parameters.get('param')` would be `foo`.\nThe `properties` function also receives other key details of the activated route, including a map of the search parameters (the part after the `?`).\n\nA route can also redirect to another path instead of rendering a component:\n```js\nconst router = new Router({\n  root: '/',\n  routes: [\n    {\n      path: 'foo',\n      redirect: 'bar'\n    },\n    {\n      path: 'bar',\n      component: DashboardComponent\n    },\n  ],\n});\n```\nThen, if the user visits `/foo`, they will automatically be redirected to `/bar` and the corresponding route will be rendered instead.\n\nRoutes can be nested inside eachother, so that components are nested when rendered:\n```js\nconst router = new Router({\n  root: '/',\n  routes: [\n    {\n      path: 'foo',\n      component: ComponentA,\n      children: [\n        {\n          path: 'bar',\n          component: ComponentB\n        },\n      ],\n    },\n  ],\n});\n```\nWhen the user visits `/foo/bar`, the components will be rendered like so:\n```html\n\u003ccomponent-a\u003e\n  \u003ccomponent-b\u003e\n  \u003c/component-b\u003e\n\u003c/component-a\u003e\n```\nNote that nested routes should not have a leading slash.\n\nIf you need your route to conditionally match based on some external value, use a route guard to determine if it should match:\n```js\nconst router = new Router({\n  root: '/',\n  routes: [\n    {\n      path: 'admin',\n      component: AdminComponent,\n      guard: () =\u003e user.isAdmin,\n    },\n  ],\n});\n```\nThis way, the route will be skipped if the guard function returns `false`.\n\nTo see more features, take a look at the `/example` directory or try it in action:\n```console\nnpm run example\n```\n\n## API\n### `class Router`\nExported as `Router` and `default`.\n- **constructor(routes: Record[]): Router**\n\n  Creates the router instance.\n- **connect(root: HTMLElement): Promise\\\u003cvoid\u003e**\n\n  Connect the router to the DOM. The promise resolves once every component has been loaded and connected.\n- **disconnect(): void**\n\n  Disconnect the router from the DOM.\n- **push(path: string, options?: NavigationOptions): Promise\\\u003cvoid\u003e**\n\n  Push a new entry onto the history stack. Resolves once every component has been loaded and connected.\n- **replace(path: string, options?: NavigationOptions): Promise\\\u003cvoid\u003e**\n\n  Replace the current entry in the history stack.\n- **pop(n: number): void**\n\n  Pop the top `n` entries in the history stack.\n\n### `interface NavigationOptions`\n- **query?: Record\u003cstring, string\u003e**\n  The search query that should be appended to the path, expressed as an object mapping keys to values.\n\n- **hash?: string**\n  The hash that should be appended to the path.\n\n### `interface RouteOptions`\n- **path: string**\n\n  This is the path that the route should match.\n  Use named parameters (`/:parameter`) to match dynamic values and pass them to the component as properties, or use a wildcard (`**`) to match anything.\n- **component?: HTMLElement**\n\n  The constructor for the component that should be rendered.\n- **load?: () =\u003e PromiseLike\u003c{ default: HTMLElement }\u003e**\n\n  Use `load` instead of `component` to lazy-load the component when the route matches for the first time.\n  Note that the component needs to be the default export of the module when using `import()`.\n- **exact?: boolean**\n\n  Whether or not the route should match *exactly*; `{ path: '/', exact: false }` would match any path (because any path begins with `/`) while `{ path: '/' exact: true }` would *only* match '/'.\n  By default, the route will match exactly when no child routes are attached, otherwise it will not.\n- **redirect?: string**\n\n  If a redirect is provided, the router will change the URL to match it and instead render the routes that match the new URL.\n- **slot?: string**\n\n  Render the component in a specific slot.\n- **guard?: () =\u003e boolean**\n\n  A function that allows selective matching; returning `false` means the route will be skipped when it would otherwise have matched.\n- **properties?: (snapshot: Snapshot) =\u003e { [key: string]: string }**\n\n  `properties` allows certain route-specific properties to be passed to the component. The snapshot contains information such as the parameters from the route, the matched path, the query and the hash.\n- **children?: RouteOptions[]**\n\n  An array of options. The paths of these nested options are appended to the parent option. Given `{ path: '/', children: [{ path: 'abc' }] }`, the path `/abc` would cause both routes to activate while the path `/` would cause only the topmost route to activate.\n\n\n### `RouterLink`\nA custom element that integrates with the `Router` like an `\u003ca\u003e` element. \n\n#### Properties\nAll properties synchronize with attributes and vice versa.\n\n- **to: string**\nThe target pathname. Similar to an `\u003ca\u003e` element's `href` attribute.\n\n- **active: boolean**\nApplied if the link's `to` attribute matches the current pathname.\n\n- **exact: boolean**\nDecides how to match the current pathname.\n\nIf `exact` is true, a link is active if `to` is strictly equal to the pathname.\nIf `exact` is false, and `to` starts with a `/`, the link is active if the current path starts with `to`.\nIf `to` does not start with a `/`, the link is active if the current path ends with `to`.\n\n- **disabled: boolean**\nIf `true`, prevents the link from triggering navigation.\n\n#### Usage\nTo use the router link, you need to first define it as a custom element:\n```js\nimport { RouterLink } from 'runway';\n\ncustomElements.define('router-link', RouterLink);\n```\nNote: you may use any name you like.\n\nThen, you can use it anywhere as a regular HTML element:\n```html\n\u003crouter-link to=\"/path\"\u003e\n  Link\n\u003crouter-link\u003e\n```\nWhen the location's pathname matches that of the element, it gains an `active` attribute, which is useful for applying styles to match.\n\nYou may wrap the element around an `\u003ca\u003e` to make use its' built in capabilities, such as ctrl-click to open in a new tab:\n```html\n\u003crouter-link to=\"/path\"\u003e\n  \u003ca\u003eLink\u003c/a\u003e\n\u003crouter-link\u003e\n```\nWhen used as a wrapper element, the `\u003crouter-link\u003e` element will automatically update the `\u003ca\u003e` element's `href` attribute whenever `to` changes.\n\nOptionally, the `\u003ca\u003e` element's `href` attribute may be set statically to mirror the `\u003crouter-link\u003e` element's `to` attribute:\n```html\n\u003crouter-link to=\"/path\"\u003e\n  \u003ca href=\"/path\"\u003eLink\u003c/a\u003e\n\u003crouter-link\u003e\n```\nThen, if JavaScript is disabled, normal navigation may still occur.\n\n## Browser support\nRunway is tested against the latest version of Chrome and Firefox.\n\n## Contributing\n\n### Development\nTo start a development environment, run:\n```console\nyarn run dev\n```\nThis starts a headless Chrome instance that will continually run tests.\n\n### Build\nTo build the project, run:\n```console\nyarn run build\n```\nThis creates the distribution files in the `dist/` directory.\n\n### Test\nTo test the project, run:\n```console\nyarn run test\n```\nThis starts a Chrome and Firefox instance that will run all the tests once per browser.\n\n### Example\nTo see an example of Runway in use, run:\n```console\nyarn run example\n```\nThis creates a local server on http://localhost:1234 that serves a demo application.\nThe application source code can be seen in the `example/` directory.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphilipahlberg%2Frunway","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphilipahlberg%2Frunway","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphilipahlberg%2Frunway/lists"}