From 791815d31f7ffdf95ce2739397177b07a49532a2 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:58:05 -0700 Subject: [PATCH 1/4] introduce jsonx components --- components/components.tsx | 162 ++++++++++++++++++++++++++++++++++++++ components/mod.ts | 4 + deno.jsonc | 10 ++- main.tsx | 0 rtx.ts | 18 ++--- 5 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 components/components.tsx create mode 100644 components/mod.ts create mode 100644 main.tsx diff --git a/components/components.tsx b/components/components.tsx new file mode 100644 index 0000000..f1bbd2b --- /dev/null +++ b/components/components.tsx @@ -0,0 +1,162 @@ +/** + * @module + * + * @example + * + * ```tsx + * if (import.meta.main) { + * const router = ( + * + * + * new Response(null, { + * status: 302, + * headers: { Location: "https://deno.com/favicon.ico" }, + * })} + * /> + * new Response("Hello, World!")} /> + * new Response("foo")} /> + * new Response("bar")} /> + * new Response("baz")} /> + * + * ); + * + * Deno.serve((request) => router.fetch(request)); + * } + * ``` + */ + +import type { + Handle as IHandle, + Method as IMethod, + Route as IRoute, +} from "rtx/mod.ts"; +import { createRouter, Router as CRouter } from "rtx/mod.ts"; + +// deno run -A components/components.tsx +// +if (import.meta.main) { + const router = ( + + + new Response(null, { + status: 302, + headers: { Location: "https://deno.com/favicon.ico" }, + })} + /> + new Response("Hello, World!")} /> + new Response("foo")} /> + new Response("bar")} /> + new Response("baz")} /> + + ); + + Deno.serve((request) => router.fetch(request)); +} + +/** + * ComponentsInterface is the interface for the components. + */ +export type ComponentsInterface = Record< + Capitalize>, + (props: RouteProps) => CRouter +>; + +/** + * RouteProps are the props for a route component. + */ +export interface RouteProps { + pattern: string; + handle: IHandle; +} + +/** + * Router is the router component. + */ +export function Router(props: { children?: unknown[] }): CRouter { + const router = createRouter(); + ((props.children) as CRouter[]) + ?.forEach((child) => { + if (child instanceof CRouter) { + router.use(child); + return; + } + + throw new Error("Invalid child of Router"); + }); + + return router; +} + +/** + * Route is the route component. + */ +export function Route(props: IRoute): CRouter { + return createRouter().with(props); +} + +/** + * Connect is the route component for a CONNECT route. + */ +export function Connect(props: RouteProps): CRouter { + return createRouter().connect(props.pattern, props.handle); +} + +/** + * Delete is the route component for a DELETE route. + */ +export function Delete(props: RouteProps): CRouter { + return createRouter().delete(props.pattern, props.handle); +} + +/** + * Get is the route component for a GET route. + */ +export function Get(props: RouteProps): CRouter { + return createRouter().get(props.pattern, props.handle); +} + +/** + * Head is the route component for a HEAD route. + */ +export function Head(props: RouteProps): CRouter { + return createRouter().head(props.pattern, props.handle); +} + +/** + * Options is the route component for a OPTIONS route. + */ +export function Options(props: RouteProps): CRouter { + return createRouter().options(props.pattern, props.handle); +} + +/** + * Patch is the route component for a PATCH route. + */ +export function Patch(props: RouteProps): CRouter { + return createRouter().patch(props.pattern, props.handle); +} + +/** + * Post is the route component for a POST route. + */ +export function Post(props: RouteProps): CRouter { + return createRouter().post(props.pattern, props.handle); +} + +/** + * Put is the route component for a PUT route. + */ +export function Put(props: RouteProps): CRouter { + return createRouter().put(props.pattern, props.handle); +} + +/** + * Trace is the route component for a TRACE route. + */ +export function Trace(props: RouteProps): CRouter { + return createRouter().trace(props.pattern, props.handle); +} diff --git a/components/mod.ts b/components/mod.ts new file mode 100644 index 0000000..f4d6a1b --- /dev/null +++ b/components/mod.ts @@ -0,0 +1,4 @@ +export * from "./components.tsx"; + +import * as components from "./components.tsx"; +components satisfies components.ComponentsInterface; diff --git a/deno.jsonc b/deno.jsonc index dc03767..fadf0fa 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -2,10 +2,18 @@ "lock": false, "name": "@fartlabs/rtx", "version": "0.0.1", - "exports": "./mod.ts", + "exports": { + ".": "./mod.ts", + "./components": "./components/mod.ts" + }, "imports": { + "@fartlabs/jsonx": "jsr:@fartlabs/jsonx@^0.0.10", "rtx/": "./" }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@fartlabs/jsonx" + }, "tasks": { "example": "deno run --allow-net examples/farm/farm.ts" } diff --git a/main.tsx b/main.tsx new file mode 100644 index 0000000..e69de29 diff --git a/rtx.ts b/rtx.ts index 0f44b2a..1db72c0 100644 --- a/rtx.ts +++ b/rtx.ts @@ -184,21 +184,21 @@ export class Router implements RouterInterface { /** * with appends a route to the router. */ - public with( - handle: Handle, - ): this; + public with(route: Route): this; public with( match: Match, handle: Handle, ): this; public with( - matchOrHandle: Match | Handle, + routeOrMatch: Match | Route, handle?: Handle, ): this { - if (typeof matchOrHandle === "function" && handle === undefined) { - this.routes.push({ handle: matchOrHandle as Handle }); - } else if (handle !== undefined) { - this.routes.push({ handle, match: matchOrHandle as Match }); + if (handle === undefined && "handle" in routeOrMatch) { + this.routes.push(routeOrMatch); + } else if (handle !== undefined && !("handle" in routeOrMatch)) { + this.routes.push({ match: routeOrMatch, handle }); + } else { + throw new Error("Invalid arguments"); } return this; @@ -234,7 +234,7 @@ export class Router implements RouterInterface { ): this { return this.with({ method: "CONNECT", - pattern: new URLPattern({ pathname: pattern }), + pattern: pattern ? new URLPattern({ pathname: pattern }) : undefined, }, handle); } From b4b20cde4cd2ea3ee5f0059f4b782912236b03dc Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:55:50 -0700 Subject: [PATCH 2/4] move core library to `@fartlabs/rt` --- README.md | 11 +- components/components.tsx | 162 ---------------- components/mod.ts | 4 - deno.jsonc | 1 + examples/farm/farm.ts | 11 -- main.tsx | 0 mod.ts | 7 +- rtx.ts | 390 ++++++++------------------------------ 8 files changed, 94 insertions(+), 492 deletions(-) delete mode 100644 components/components.tsx delete mode 100644 components/mod.ts delete mode 100644 examples/farm/farm.ts delete mode 100644 main.tsx diff --git a/README.md b/README.md index dcc1d74..bb9459f 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,14 @@ [![GitHub Actions][GitHub Actions badge]][GitHub Actions] -Minimal HTTP router library based on the `URLPattern` API. +Minimal HTTP router library based on the `URLPattern` API in JSX. ## Usage -```ts +```tsx import { createRouter } from "@fartlabs/rtx"; -const router = createRouter() - .get("/", () => { - return new Response("Hello, World!"); - }) - .default(() => new Response("Not found", { status: 404 })); - +const router = new Response("Hello, World!")} />; Deno.serve((request) => router.fetch(request)); ``` diff --git a/components/components.tsx b/components/components.tsx deleted file mode 100644 index f1bbd2b..0000000 --- a/components/components.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @module - * - * @example - * - * ```tsx - * if (import.meta.main) { - * const router = ( - * - * - * new Response(null, { - * status: 302, - * headers: { Location: "https://deno.com/favicon.ico" }, - * })} - * /> - * new Response("Hello, World!")} /> - * new Response("foo")} /> - * new Response("bar")} /> - * new Response("baz")} /> - * - * ); - * - * Deno.serve((request) => router.fetch(request)); - * } - * ``` - */ - -import type { - Handle as IHandle, - Method as IMethod, - Route as IRoute, -} from "rtx/mod.ts"; -import { createRouter, Router as CRouter } from "rtx/mod.ts"; - -// deno run -A components/components.tsx -// -if (import.meta.main) { - const router = ( - - - new Response(null, { - status: 302, - headers: { Location: "https://deno.com/favicon.ico" }, - })} - /> - new Response("Hello, World!")} /> - new Response("foo")} /> - new Response("bar")} /> - new Response("baz")} /> - - ); - - Deno.serve((request) => router.fetch(request)); -} - -/** - * ComponentsInterface is the interface for the components. - */ -export type ComponentsInterface = Record< - Capitalize>, - (props: RouteProps) => CRouter ->; - -/** - * RouteProps are the props for a route component. - */ -export interface RouteProps { - pattern: string; - handle: IHandle; -} - -/** - * Router is the router component. - */ -export function Router(props: { children?: unknown[] }): CRouter { - const router = createRouter(); - ((props.children) as CRouter[]) - ?.forEach((child) => { - if (child instanceof CRouter) { - router.use(child); - return; - } - - throw new Error("Invalid child of Router"); - }); - - return router; -} - -/** - * Route is the route component. - */ -export function Route(props: IRoute): CRouter { - return createRouter().with(props); -} - -/** - * Connect is the route component for a CONNECT route. - */ -export function Connect(props: RouteProps): CRouter { - return createRouter().connect(props.pattern, props.handle); -} - -/** - * Delete is the route component for a DELETE route. - */ -export function Delete(props: RouteProps): CRouter { - return createRouter().delete(props.pattern, props.handle); -} - -/** - * Get is the route component for a GET route. - */ -export function Get(props: RouteProps): CRouter { - return createRouter().get(props.pattern, props.handle); -} - -/** - * Head is the route component for a HEAD route. - */ -export function Head(props: RouteProps): CRouter { - return createRouter().head(props.pattern, props.handle); -} - -/** - * Options is the route component for a OPTIONS route. - */ -export function Options(props: RouteProps): CRouter { - return createRouter().options(props.pattern, props.handle); -} - -/** - * Patch is the route component for a PATCH route. - */ -export function Patch(props: RouteProps): CRouter { - return createRouter().patch(props.pattern, props.handle); -} - -/** - * Post is the route component for a POST route. - */ -export function Post(props: RouteProps): CRouter { - return createRouter().post(props.pattern, props.handle); -} - -/** - * Put is the route component for a PUT route. - */ -export function Put(props: RouteProps): CRouter { - return createRouter().put(props.pattern, props.handle); -} - -/** - * Trace is the route component for a TRACE route. - */ -export function Trace(props: RouteProps): CRouter { - return createRouter().trace(props.pattern, props.handle); -} diff --git a/components/mod.ts b/components/mod.ts deleted file mode 100644 index f4d6a1b..0000000 --- a/components/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./components.tsx"; - -import * as components from "./components.tsx"; -components satisfies components.ComponentsInterface; diff --git a/deno.jsonc b/deno.jsonc index fadf0fa..b80a956 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -8,6 +8,7 @@ }, "imports": { "@fartlabs/jsonx": "jsr:@fartlabs/jsonx@^0.0.10", + "@fartlabs/rt": "jsr:@fartlabs/rt@^0.0.1", "rtx/": "./" }, "compilerOptions": { diff --git a/examples/farm/farm.ts b/examples/farm/farm.ts deleted file mode 100644 index 11faaa5..0000000 --- a/examples/farm/farm.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createRouter } from "rtx/mod.ts"; - -if (import.meta.main) { - const router = createRouter() - .get<"id">("/animals/:id", (ctx) => { - return new Response(`Animal ID: ${ctx.params.id}`); - }) - .default(() => new Response("Not found", { status: 404 })); - - Deno.serve((request) => router.fetch(request)); -} diff --git a/main.tsx b/main.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/mod.ts b/mod.ts index a17f4e6..e02d2ae 100644 --- a/mod.ts +++ b/mod.ts @@ -1,7 +1,12 @@ /** * @module * - * rtx is a minimalistic HTTP router library based on the `URLPattern` API. + * rtx is a minimalistic HTTP router library based on the `URLPattern` API in + * JSX. */ +import * as rtx from "./rtx.ts"; + +rtx satisfies rtx.ComponentsInterface; + export * from "./rtx.ts"; diff --git a/rtx.ts b/rtx.ts index 1db72c0..c246b5e 100644 --- a/rtx.ts +++ b/rtx.ts @@ -1,344 +1,122 @@ +import type { + Handle as IHandle, + Method as IMethod, + Route as IRoute, +} from "@fartlabs/rt"; +import { createRouter, Router as CRouter } from "@fartlabs/rt"; + /** - * createRouter creates a new router. + * ComponentsInterface is the interface for the components. */ -export function createRouter(fn?: (r: Router) => Router): Router { - const router = new Router(); - if (fn) { - return fn(router); - } - - return router; -} +export type ComponentsInterface = Record< + Capitalize>, + (props: RouteProps) => CRouter +>; /** - * METHODS is the list of HTTP methods. + * RouteProps are the props for a route component. */ -export const METHODS = [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE", -] as const; +export interface RouteProps { + pattern: string; + handle: IHandle; +} /** - * Method is a type which represents an HTTP method. + * RouterProps are the props for the router component. */ -export type Method = typeof METHODS[number]; +export interface RouterProps { + children?: unknown[]; + default?: IHandle; +} /** - * Match is a type which matches a Request object. + * Router is the router component. */ -export type Match = - | ((detail: { request: Request; url: URL }) => Promise) - | { - /** - * pattern is the URL pattern to match on. - */ - pattern?: URLPattern; +export function Router(props: RouterProps): CRouter { + const router = createRouter(); + ((props.children) as CRouter[]) + ?.forEach((child) => { + if (child instanceof CRouter) { + router.use(child); + return; + } - /** - * method is the HTTP method to match on. - */ - method?: Method; - }; + throw new Error("Invalid child of Router"); + }); + + if (props.default) { + router.default(props.default); + } + + return router; +} /** - * Handle is called to handle a request. + * Route is the route component. */ -export interface Handle { - (ctx: RouterContext): Promise | Response; +export function Route(props: IRoute): CRouter { + return createRouter().with(props); } /** - * Route represents a the pairing of a matcher and a handler. + * Connect is the route component for a CONNECT route. */ -export interface Route { - /** - * handle is called to handle a request. - */ - handle: Handle; - - /** - * match is called to match a request. - */ - match?: Match; +export function Connect(props: RouteProps): CRouter { + return createRouter().connect(props.pattern, props.handle); } /** - * Routes is a sequence of routes. + * Delete is the route component for a DELETE route. */ -export type Routes = Route[]; +export function Delete(props: RouteProps): CRouter { + return createRouter().delete(props.pattern, props.handle); +} /** - * RouterContext is the object passed to a router. + * Get is the route component for a GET route. */ -export interface RouterContext { - /** - * request is the original request object. - */ - request: Request; - - /** - * url is the parsed fully qualified URL of the request. - */ - url: URL; - - /** - * params is a map of matched parameters from the URL pattern. - */ - params: { [key in T]: string }; - - /** - * next executes the next matched route in the sequence. If no more routes are - * matched, the default handler is called. - */ - next: () => Promise; +export function Get(props: RouteProps): CRouter { + return createRouter().get(props.pattern, props.handle); } /** - * RouterInterface is the interface for a router. + * Head is the route component for a HEAD route. */ -type RouterInterface = Record< - Lowercase, - ((pattern: string, handle: Handle) => Router) ->; +export function Head(props: RouteProps): CRouter { + return createRouter().head(props.pattern, props.handle); +} /** - * Router is an HTTP router based on the `URLPattern` API. + * Options is the route component for a OPTIONS route. */ -export class Router implements RouterInterface { - public routes: Routes = []; - public defaultHandle?: Handle; - - /** - * fetch invokes the router for the given request. - */ - public async fetch(request: Request, i = 0): Promise { - const url = new URL(request.url); - while (i < this.routes.length) { - const route = this.routes[i]; - const matchedMethod = route.match === undefined || - typeof route.match !== "function" && - (route.match.method === undefined || - route.match.method === request.method); - if (!matchedMethod) { - i++; - continue; - } - - const matchedFn = typeof route.match === "function" && - await route.match({ request, url }); - const matchedPattern = route.match !== undefined && - typeof route.match !== "function" && - route.match.pattern !== undefined && - route.match.pattern.exec(request.url); - let params: Record = {}; - if (matchedPattern) { - params = matchedPattern?.pathname - ? Object.entries(matchedPattern.pathname.groups) - .reduce( - (groups, [key, value]) => { - if (value !== undefined) { - groups[key] = value; - } - - return groups; - }, - {} as { [key: string]: string }, - ) - : {}; - } - - // If the route matches, call it and return the response. - if (route.match === undefined || matchedFn || matchedPattern) { - return await route.handle({ - request, - url, - params, - next: () => this.fetch(request, i + 1), - }); - } - - i++; - } - - if (this.defaultHandle !== undefined) { - return await this.defaultHandle({ - request, - url, - params: {}, - next: () => { - throw new Error("next() called from default handler"); - }, - }); - } - - throw new Error("Not found"); - } - - /** - * with appends a route to the router. - */ - public with(route: Route): this; - public with( - match: Match, - handle: Handle, - ): this; - public with( - routeOrMatch: Match | Route, - handle?: Handle, - ): this { - if (handle === undefined && "handle" in routeOrMatch) { - this.routes.push(routeOrMatch); - } else if (handle !== undefined && !("handle" in routeOrMatch)) { - this.routes.push({ match: routeOrMatch, handle }); - } else { - throw new Error("Invalid arguments"); - } - - return this; - } - - /** - * use appends a sequence of routers to the router. - */ - public use(data: Routes | Router): this { - if (data instanceof Router) { - this.routes.push(...data.routes); - } else { - this.routes.push(...data); - } - - return this; - } - - /** - * default sets the router's default handler. - */ - public default(handle: Handle | undefined): this { - this.defaultHandle = handle; - return this; - } - - /** - * connect appends a router for the CONNECT method to the router. - */ - public connect( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "CONNECT", - pattern: pattern ? new URLPattern({ pathname: pattern }) : undefined, - }, handle); - } - - /** - * delete appends a router for the DELETE method to the router. - */ - public delete( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "DELETE", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } - - /** - * get appends a router for the GET method to the router. - */ - public get( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "GET", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } - - /** - * head appends a router for the HEAD method to the router. - */ - public head( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "HEAD", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } - - /** - * options appends a router for the OPTIONS method to the router. - */ - public options( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "OPTIONS", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } +export function Options(props: RouteProps): CRouter { + return createRouter().options(props.pattern, props.handle); +} - /** - * patch appends a router for the PATCH method to the router. - */ - public patch( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "PATCH", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } +/** + * Patch is the route component for a PATCH route. + */ +export function Patch(props: RouteProps): CRouter { + return createRouter().patch(props.pattern, props.handle); +} - /** - * post appends a router for the POST method to the router. - */ - public post( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "POST", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } +/** + * Post is the route component for a POST route. + */ +export function Post(props: RouteProps): CRouter { + return createRouter().post(props.pattern, props.handle); +} - /** - * put appends a router for the PUT method to the router. - */ - public put( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "PUT", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } +/** + * Put is the route component for a PUT route. + */ +export function Put(props: RouteProps): CRouter { + return createRouter().put(props.pattern, props.handle); +} - /** - * trace appends a router for the TRACE method to the router. - */ - public trace( - pattern: string, - handle: Handle, - ): this { - return this.with({ - method: "TRACE", - pattern: new URLPattern({ pathname: pattern }), - }, handle); - } +/** + * Trace is the route component for a TRACE route. + */ +export function Trace(props: RouteProps): CRouter { + return createRouter().trace(props.pattern, props.handle); } From 33fda54645b2606f55efe9a52ce274a69413c943 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:34:03 -0700 Subject: [PATCH 3/4] Update deno.jsonc --- deno.jsonc | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index b80a956..6a9bb1a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -2,20 +2,13 @@ "lock": false, "name": "@fartlabs/rtx", "version": "0.0.1", - "exports": { - ".": "./mod.ts", - "./components": "./components/mod.ts" - }, + "exports": "./mod.ts", "imports": { "@fartlabs/jsonx": "jsr:@fartlabs/jsonx@^0.0.10", - "@fartlabs/rt": "jsr:@fartlabs/rt@^0.0.1", - "rtx/": "./" + "@fartlabs/rt": "jsr:@fartlabs/rt@^0.0.1" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@fartlabs/jsonx" - }, - "tasks": { - "example": "deno run --allow-net examples/farm/farm.ts" } } From ef8e9ae36e7e063f2065d9b8cdb0797a5197b33d Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:37:03 -0700 Subject: [PATCH 4/4] Update README.md --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bb9459f..fe38a9b 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,20 @@ [![GitHub Actions][GitHub Actions badge]][GitHub Actions] +> [!NOTE] +> +> Built with [**@fartlabs/rt**](https://github.com/FartLabs/rt) and +> [**@fartlabs/jsonx**](https://github.com/FartLabs/jsonx). + Minimal HTTP router library based on the `URLPattern` API in JSX. ## Usage ```tsx -import { createRouter } from "@fartlabs/rtx"; +import { Get } from "@fartlabs/rtx"; const router = new Response("Hello, World!")} />; + Deno.serve((request) => router.fetch(request)); ``` @@ -41,11 +47,11 @@ Run `deno lint` to lint the code. --- -Developed with ❤️ by [**@EthanThatOneKid**](https://etok.codes/) +Developed with ❤️ [**@FartLabs**](https://github.com/FartLabs) [JSR]: https://jsr.io/@fartlabs/rtx [JSR badge]: https://jsr.io/badges/@fartlabs/rtx [JSR score]: https://jsr.io/@fartlabs/rtx/score [JSR score badge]: https://jsr.io/badges/@fartlabs/rtx/score -[GitHub Actions]: https://github.com/EthanThatOneKid/rtx/actions/workflows/check.yaml -[GitHub Actions badge]: https://github.com/EthanThatOneKid/rtx/actions/workflows/check.yaml/badge.svg +[GitHub Actions]: https://github.com/FartLabs/rtx/actions/workflows/check.yaml +[GitHub Actions badge]: https://github.com/FartLabs/rtx/actions/workflows/check.yaml/badge.svg