{"id":13678200,"url":"https://github.com/marvelbark2/ryo-js","last_synced_at":"2025-04-10T18:32:06.457Z","repository":{"id":63437280,"uuid":"566766217","full_name":"marvelbark2/ryo-js","owner":"marvelbark2","description":"Js fullstack framework, Incredibly fast ","archived":false,"fork":false,"pushed_at":"2024-10-05T07:29:14.000Z","size":1652,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-24T16:11:09.747Z","etag":null,"topics":["blog","browser","compiler","components","framework-js","frameworks","fullstack","graphql","hybrid","javascript","node","preact","react","reactjs","rest-api","server","static-site-generator","typescript","universal","websocket"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/marvelbark2.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"license","code_of_conduct":"CODE_OF_CONDUCT.md","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":"2022-11-16T11:27:54.000Z","updated_at":"2024-10-07T14:57:09.000Z","dependencies_parsed_at":"2023-11-06T05:26:34.594Z","dependency_job_id":"b8a1dc6f-39a8-4916-b659-05e9bb9a2d8e","html_url":"https://github.com/marvelbark2/ryo-js","commit_stats":{"total_commits":37,"total_committers":2,"mean_commits":18.5,"dds":0.08108108108108103,"last_synced_commit":"2bca74268cd11b9635e374180eba3db3e6b26b69"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marvelbark2%2Fryo-js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marvelbark2%2Fryo-js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marvelbark2%2Fryo-js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marvelbark2%2Fryo-js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marvelbark2","download_url":"https://codeload.github.com/marvelbark2/ryo-js/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247918918,"owners_count":21018044,"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":["blog","browser","compiler","components","framework-js","frameworks","fullstack","graphql","hybrid","javascript","node","preact","react","reactjs","rest-api","server","static-site-generator","typescript","universal","websocket"],"created_at":"2024-08-02T13:00:51.016Z","updated_at":"2025-04-10T18:32:06.437Z","avatar_url":"https://github.com/marvelbark2.png","language":"TypeScript","funding_links":[],"categories":["Uncategorized"],"sub_categories":["Uncategorized"],"readme":"# Ryo js\n\nSmall js fullstack framework blazly fast\n**Memo version**\n## Installation\n\n```sh\nnpm i ryo.js #or npm i ryo.js@github:marvelbark2/ryo-js\n```\n\n## Features:\n- Routing based filesystem\n- Blazly fast (Try it by yourself)\n- Everything on src folder\n- Create apis, websockets, graphQL, Server-Sent-Events (SSE), preact components and serve static files.\n- SPA routing which makes the site so fast (Using Flamethrower)\n- Typescript (No types generated at least for now) supported without configuration needed (Example: https://github.com/marvelbark2/ryo-js-examples/blob/main/ryo-api/src/me.ts)\n\n## what you can do with Ryo js:\n\n* Routing system: Based on filesystem, you can build dynamic route, naming file with \":\" prefix\n    \n* Preact Components:\n    * Static Component (Sync data fetching): export data method returning a value\n    * Static Component (Async data fetching): export data object contains:\n        - runner: Function async accepts stop method as argument (stop: called to stop caching) returns a value\n        - invalidate(Optional): Field, duration per second to cache value, it's a global value.\n        - shouldUpdate(Optional): Function accepts two values (Old, new) to re-render component on runtime when data changed after the cache invalidated\n        - You can also use file as datasource by specifying the file path in data field also specify parsing way\n    * Server Component (TODO): export server method without returning anything\n        - Here you can use async functional Component and use nodejs api and use JSX synthax but no client side will be run (Hooks, document ... will be ignored)\n    * Parent Component: For each component type described before, you can wrap them with a component independent state, you can either add `entry.jsx` as global wrapper or you can add it to the component itself by exporting the component naming it `Parent` (Check the ex `Static async/fresh component` down below). If both used, the parent component declared in the component itself will be used. (If you're using refreshed Static async/fresh component, you should provide the id passed as parent component props in jsx/html element that will be used to revalidate the component after data updated)\n      \n      * Each component could have a offline version but just export offline as component function that will be used when the client is offline\n\n* Api: export function with method name, like: ``` export get() { return ... }  ```\n    * JSON api: By returning js objects parsable values\n    * Streamable api: By returning object:\n        - stream: Created stream, like readStream\n        - length: Stream length (without reading it)\n    * You can build versionable apis where you can name file like **service@1|2|...|n.(js|ts)**. Client-side, pass in http request header, a version as value for the key **X-API-VERSION**\n    * GraphQL endpoints (Still fixing subscriptions): You can build many graphql endpoint with separated schema by naming the route with this extension .gql.(ts | js)\n        \n* Websockets: naming the file in src folder with \".ws.js\" suffix:\n    - Return object match uWebSockets.js documentation\n* Server-Sent-Events: naming the file in src folder with \".ev.js\" suffix:\n    - Export default: object with invalidate field (ms) and runner function (Async with params route if needed)\n* Subdomains: You can create subdomains by creating a folder with the _subdomains name in src folder and add index.js or index.ts file in it. (Example: _subdomains/api/index.ts) (You can use it to create a subdomain for your api) (You can also create dynamic subdomains by naming the folder with \":\" prefix)\n* Errors pages: You can create error pages by creating a folder with the _errors name in src folder and code error with tsx or jsx extension. (Example: _errors/4XX.tsx) (You can handle all the error that error number starts with 4 like 404, 413 ...)\n* Security headers (2 complete): You can handle authentification and authorization by using ryo.config.js.\n  \n   Example with jwt authentification and subdomain for blog project:\n    ```js\n    // file: ryo.config.js\n    /** @type {import('ryo.js').RyoConfig} */\n\n    const bcrypt = require(\"bcrypt\");\n    const jsonwebtoken = require(\"jsonwebtoken\");\n\n    const BASE_HOST = process.env.BASE_HOST || \"localhost:3000\";\n\n    const jwtAuthFilter = {\n        doFilter(request, _response, setAuthContext, next) {\n            const authHeader = request.getHeader(\"authorization\");\n            if (authHeader.length === 0 || !authHeader.startsWith(\"Bearer \")) {\n                return next();\n            }\n\n            const jwt = authHeader.substring(7);\n            const decoded = jsonwebtoken.verify(jwt, \"secret\");\n\n            setAuthContext({\n                id: decoded.username,\n                roles: decoded.roles,\n            })\n\n            return next();\n        }\n    }\n\n    module.exports = {\n        subdomain: {\n            baseHost: BASE_HOST,\n        },\n        security: {\n            cors: false,\n            csrf: true,\n            authorizeHttpRequests: [\n                {\n                    path: [\"/\", \"/*.{js,css}\", \"/images/*\", \"/blog\", \"/_subdomain/test/**/*.{js,css}\", \"/_subdomain/**/test\"],\n                    status: \"allow\",\n                },\n                {\n                    path: [\"/blog/ff\", \"/_subdomain/test/page\"],\n                    status: \"auth\",\n                },\n                {\n                    path: \"/_subdomain/test/\",\n                    roles: [\"admin\"],\n                }\n\n            ],\n            authProvider: {\n                async loadUserByUsername(username) {\n                    return {\n                        username,\n                        plainTextPassword: bcrypt.hashSync(\"123456\", 10),\n                        roles: [username],\n                        onLoginSuccess: (res) =\u003e {\n                            const jwt = jsonwebtoken.sign({\n                                username,\n                                roles: [username]\n                            }, \"secret\");\n\n                            console.log({\n                                jwt, res\n                            });\n                            res.writeHeader(\"Authorization\", `Bearer ${jwt}`);\n\n                            res.end(\"Done\")\n                        }\n                    }\n                },\n                passwordEncoder: {\n                    encode: async (password) =\u003e bcrypt.hash(password, 10),\n                    matches: async (password, hash) =\u003e bcrypt.compare(password, hash),\n                }\n            },\n            sessionManagement: {\n                sessionCreationPolicy: \"stateless\",\n            },\n            filter: [\n                jwtAuthFilter\n            ]\n        },\n    }\n\n    ```\n## Progress Status:\n- [ ] Preact Components\n  - [X] Async static component\n  - [X] Sync static component\n  - [X] Server Component\n  - [X] Server Component with hooks\n  - [ ] Offline version\n    - [X] Offline version local\n    - [ ] Offline version global\n- [X] Api\n  - [X] JSON api\n  - [X] Readable stream api\n\n  - [ ] API tools \n    - [ ] generate api types on client side for type safe\n- [X] GraphQL\n  - [X] Query\n  - [X] Mutation\n  - [X] Subscription\n  - [X] Playground on dev mode\n- [X] Websockets\n- [X] Server-Sent-Events\n- [ ] Subdomains\n  - [X] Static subdomains\n  - [ ] Dynamic subdomains\n  - [X] Api\n  - [ ] GraphQL\n  - [ ] SSE\n  - [ ] Websockets\n- [ ] Security context:\n## Example:\n\n### websockets:\n```js\n// Path: src/msg.ws.js\nexport default {\n    open: (ws, req) =\u003e {\n        console.log(\"NEW CLIENT on /msg\");\n    },\n\n    message: (ws, message, isBinary) =\u003e { },\n\n    close: (ws, code, message) =\u003e {\n\n    }\n}\n```\n### API\n\n#### JSON API:\n\n```js\n// Path: src/api.js\n\nexport function get({ url }) {\n    return {\n        message: \"Hello from \" + url\n    };\n}\n\n//body: object(json input) | Buffer(buffer array input) | undefined(none)\nexport function post({ body }) {\n    // do something using body\n    console.log({ body });\n    return {\n        message: \"Hello from \" + (typeof body)\n    };\n}\n```\n\n#### Streamable API:\n```js\n// Path: src/file.js\n\nimport fs from 'fs';\nimport { join } from 'path';\n\nexport function get({ url }) {\n    const path = join(process.cwd(), \"./screen.mov\");\n    const stream = fs.createReadStream(path)\n    stream.on(\"error\", () =\u003e {\n        //Handle error to avoid server crash\n        console.log(\"error\");\n    })\n    const length = fs.statSync(path).size;\n    return {\n        stream, length\n    };\n}\n```\n\n#### GraphQL endpoint:\n\n````js\n// path: ttql.gql.ts\nexport default {\n  schema: `\n    type Query {\n      hello: String\n    }\n    type Mutation {\n      capitalize(message: String): String\n    }\n    `,\n  resolvers: {\n    hello: (_: unknown, ctx: { test: string }) =\u003e `${ctx.test}: hello world`,\n    capitalize: ({ message }: { message: string }) =\u003e message.toUpperCase()\n  },\n  context: {\n    test: \"ME\"\n  }\n}\n\n````\nuse NODE_ENV=development to access graphql playground in GET request as example: /ttql.gql\n\n##### Subscription:\n```typescript\n\nimport { PubSub } from 'graphql-subscriptions'\n\nconst TODOS_CHANNEL = \"TODOS_CHANNEL\";\n\nconst pubsub = new PubSub();\nconst todos = [\n    {\n        id: \"1\",\n        text: \"Learn GraphQL + Soild\",\n        done: false,\n    },\n];\n\nconst typeDefs = `\n    type Todo {\n      id: ID!\n      done: Boolean!\n      text: String!\n    }\n    type Query {\n      getTodos: [Todo]!\n    }\n    type Mutation {\n      addTodo(text: String!): Todo\n      setDone(id: ID!, done: Boolean!): Todo\n    }\n    type Subscription {\n      todos: [Todo]!\n    }\n  `;\n\nconst resolvers = {\n    getTodos: () =\u003e {\n        return todos;\n    },\n\n    addTodo: (\n        { text }: { text: string },\n        { pubsub }: { pubsub: PubSub }\n    ) =\u003e {\n        const newTodo = {\n            id: String(todos.length + 1),\n            text,\n            done: false,\n        };\n\n        todos.push(newTodo);\n        pubsub.publish(TODOS_CHANNEL, { todos });\n        return newTodo;\n    },\n    setDone: (\n        { id, done }: { id: string; done: boolean },\n        { pubsub }: { pubsub: PubSub }\n    ) =\u003e {\n        const todo = todos.find((todo) =\u003e todo.id === id);\n        if (!todo) {\n            throw new Error(\"Todo not found\");\n        }\n        todo.done = done;\n        pubsub.publish(TODOS_CHANNEL, { todos });\n        return todo;\n    },\n\n    todos: (_: unknown, { pubsub }: { pubsub: PubSub }) =\u003e {\n        const iterator = pubsub.asyncIterator(TODOS_CHANNEL);\n        pubsub.publish(TODOS_CHANNEL, { todos });\n        return iterator;\n    },\n}\nexport default {\n    schema: typeDefs,\n    resolvers: resolvers,\n    context: {\n        pubsub\n    }\n}\n```\n### Preact components:\n#### Server components:\n```js\n//Path: src/server.jsx\n\nexport function server({ req }) {\n    return {\n        status: 201,\n        headers: {\n            \"X-TEST\": \"YES\",\n        },\n        body: {\n            \"From\": \"SERVER\",\n        }\n    }\n}\n\nexport default function index({ data }) {\n    return (\n        \u003cdiv\u003e\n            \u003ch1\u003eServer Side Rendering \u003cb\u003e\u003c/b\u003e\u003c/h1\u003e\n\n            \u003cp\u003eFrom: {data.From}\u003c/p\u003e\n        \u003c/div\u003e\n    )\n}\n```\n#### Static sync component:\n```js\n// Path: src/index.jsx\n// route: /\n\nimport { useEffect, useState } from \"react\";\n\n// Server side function\nexport function data() {\n    return {\n        \"counter\": 3,\n    }\n}\nexport default function index({ data }) {\n    const [count, setCount] = useState(data.counter);\n   \n    useEffect(() =\u003e {\n        window.addEventListener('flamethrower:router:fetch-progress', ({ detail }) =\u003e {\n            console.log('Fetch Progress:', detail);\n        });\n\n    }, [])\n    return (\n        \u003cdiv className=\"w-screen h-screen flex items-center justify-center bg-gray-50\"\u003e\n            \u003cdiv className=\"flex flex-col w-full p-10 mx-24 border border-dashed border-gray-500 space-y-6 items-center\"\u003e\n                \u003cp\u003eYou clicked \u003cspan className=\"font-bold text-lg text-gray-800\"\u003e{count}\u003c/span\u003e times\u003c/p\u003e\n                \u003cbutton className=\"bg-blue-50 p-3 border-blue-700 text-blue-700 w-24 rounded-xl\" onClick={() =\u003e setCount(count + 1)}\u003eClick me\u003c/button\u003e\n\n                {/* SPA routing thanks to: Flamethrower */}\n                \u003ca href=\"/data\"\u003eData\u003c/a\u003e\n                \u003ca href=\"/api\"\u003eTEST\u003c/a\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n    )\n}\n```\n\n#### Static async/fresh component:\n#### Runner Fn:\n```js\n// Path: src/counter.jsx\n// route: /counter\ntype CounterDataType = { value: number, date: Date };\n\nlet count = 0;\nexport const data = {\n    invalidate: 1,\n    shouldUpdate: (_old: CounterDataType, newValue: CounterDataType) =\u003e newValue.value \u003e 10,\n    runner: async (stop: () =\u003e void, old?: CounterDataType) =\u003e {\n        if (old?.count === 60) {\n            stop();\n        }\n        return {\n            value: count++,\n            date: new Date()\n        };\n    }\n}\n\n// Parent Layout\nexport function Parent({ children }: { children: any }) {\n    return (\n        \u003cdiv\u003e\n            \u003ch1\u003eParent\u003c/h1\u003e\n            {children}\n        \u003c/div\u003e\n    )\n}\nexport default function index({ data }: { data: CounterDataType }) {\n    return (\n        \u003cdiv\u003e\n            \u003cp\u003e\n                COUNTING at {data.date.getTime()} ... {data.value}\n            \u003c/p\u003e\n        \u003c/div\u003e\n    )\n}\n```\n\n```js\n//path: src/data.jsx\n//route: /data\n\n// Other example \u0026 router\n\nimport { PrismaClient } from '@prisma/client'\nimport { useEffect } from 'react';\nimport Btn from '../comp/Btn';\nimport Router from '../lib/router/router';\n\nexport const data = {\n    invalidate: 1000,\n    runner: async (stop) =\u003e {\n        const prisma = new PrismaClient();\n        const events = await prisma.event.findMany({});\n        return { events };\n    }\n}\nexport default function index({ data }) {\n    const router = Router();\n    useEffect(() =\u003e {\n        console.log({ data });\n    }, [])\n    if (router.isLoading) return \u003cdiv\u003eLoading...\u003c/div\u003e\n    return (\n        \u003cdiv\u003e\n            \u003cp\u003e\n                \u003cspan onClick={() =\u003e router.back()}\u003eBACK\u003c/span\u003e\n                \u003cBtn text=\"TEST\" /\u003e\n                {\n                    data.events.map((event) =\u003e {\n                        return (\n                            \u003cdiv key={event.id}\u003e\n                                \u003ca href={`/blog/${event.id}`}\u003e{event.name}\u003c/a\u003e\n                            \u003c/div\u003e\n                        )\n                    })\n                }\n            \u003c/p\u003e\n        \u003c/div\u003e\n    )\n}\n```\n#### Source file:\n```js\nimport Articles from \"@/components/Articles\";\nimport type { Article } from \"@/types\";\nimport type { RyoDataObject } from \"ryo.js\";\n\nexport const data: RyoDataObject = {\n    source: {\n        // root path is CWD\n        file: \"data/articles.json\",\n    },\n    invalidate: 20,\n    shouldUpdate: () =\u003e true\n}\nexport default function App({ data }: { data: Article[] }) {\n    return (\n        \u003cArticles articles={data} /\u003e\n    )\n}\n```\n### Dynamic route (component):\n\n```js\n//path: src/blog/:id.jsx\n//route: /blog/ID\n\nimport { useEffect } from \"react\";\nimport Router from \"ryo.js/router\"\n\nexport default function index({ ...props }) {\n    const router = Router();\n\n    if (router.isLoading) return \u003cdiv\u003eLoading...\u003c/div\u003e\n    return (\n        \u003cdiv\u003e\n            Blog id: {router.query.id}\n            \u003cspan \u003eReturn Back\u003c/span\u003e\n        \u003c/div\u003e\n    )\n}\n```\n\n\n```js\n//path: src/events/:id.ev.js\n//route: /events/ID.ev\n\nexport default {\n    invalidate: 1000,\n    runner: async ({ params }) =\u003e {\n        console.log(params)\n        return { message: \"I'm the user: \" + params.id };\n    }\n}\n\n\n```\n\n### Offline component:\n\nin the example suppose the display the default component if the user is online and the offline component if the user is offline (or if the server is down)\n```js\nimport { useEffect, useState } from \"react\";\nimport { test } from \"@lib/db-exec\";\nimport toast, { Toaster } from 'react-hot-toast';\n\nfunction data() {\n    test()\n    //test()\n    return {\n        \"counter\": 3,\n        saveOnData(data: any) {\n            console.log(data)\n        }\n    }\n}\n\n// Offline component\nexport function offline() {\n    const [counter, setCounter] = useState(0);\n    return (\n        \u003cdiv className=\"bg-gray-50\"\u003e\n            \u003cToaster\n                position=\"top-right\"\n                toastOptions={{\n                    duration: 10000,\n                }}\n            /\u003e\n\n            \u003cbutton className=\"px-4 py-2 bg-blue-900 text-white rounded-2xl\" onClick={() =\u003e toast.success(\"Good\")} \u003eToast\u003c/button\u003e\n            \u003cbutton className=\"mx-3 px-4 py-2 text-blue-900 border-blue-900 border bg-white rounded-2xl\"\n                onClick={() =\u003e setCounter(c =\u003e c + 1)}\n            \u003e\n                counting \u003cb\u003e{counter}\u003c/b\u003e\n            \u003c/button\u003e\n\n        \u003c/div\u003e\n    )\n}\n\n\nexport default function index({ data }: { data: { counter: number, saveOnData: (data: any) =\u003e void } }) {\n    const [count, setCount] = useState(data.counter);\n    useEffect(() =\u003e {\n        console.log(\"Hello from client side: \", data)\n    }, [data]);\n\n    return (\n        \u003cdiv className=\"w-screen h-screen flex items-center justify-center bg-gray-50\"\u003e\n            \u003cToaster\n                position=\"top-right\"\n                toastOptions={{\n                    duration: 10000,\n                }}\n            /\u003e\n            \u003cdiv className=\"flex flex-col w-full p-10 mx-24 border border-dashed border-gray-500 space-y-6 items-center\"\u003e\n                \u003cp\u003eYou clicked \u003cspan className=\"font-bold text-lg text-gray-800\"\u003e{count}\u003c/span\u003e times\u003c/p\u003e\n                \u003cbutton className=\"bg-blue-50 p-3 border-blue-700 text-blue-700 w-24 rounded-xl\" onClick={() =\u003e setCount(count + 1)}\u003eClick me\u003c/button\u003e\n\n                \u003cspan className=\"hover:cursor-pointer\" onClick={() =\u003e toast.success('HOL')}\u003eTOAST click\u003c/span\u003e\n\n                {/* SPA routing thanks to: Flamethrower */}\n                \u003ca href=\"/data\"\u003eData\u003c/a\u003e\n                \u003ca href=\"/api\"\u003eTEST\u003c/a\u003e\n                \u003cbutton onClick={() =\u003e data.saveOnData({ a: \"me\" })}\u003eAPI TEST\u003c/button\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n    )\n}\n\nexport {\n    data\n}\n```\n### Middleware:\nYou can add middleware by adding a file in root of the project: **middleware.(ts|js)**\nYou can also have catch errors by using last argument of the middleware function which could be null. You can also add structure to init stuff like database connection or other stuff. by exporting init function \n#### Example:\n\n```js\n//path: middleware.js\n\nconst getCookie = (req, name) =\u003e \n(req.cookies ??= req.getHeader('cookie')).match(getCookie[name] ??= new RegExp(`(^|;)\\\\s*${name}\\\\s*=\\\\s*([^;]+)`))?.[2]\n\nexport default function middleware(req, res, next, error) {\n    const user = getCookie(req, \"user\")\n    if (user) {\n        return next();\n    } else {\n        res.writeHeader(\"Set-Cookie\", \"user=Guest\")\n        return res.writeStatus(\"401\").end(\"You are not authorized\")\n    }\n}\n\n//Optional\nexport function init({ context }: { context: Map\u003cstring, any\u003e }) {\n    \n    const db = ... // Inmemory database or other stuff\n    context.set(\"articleDb\", db); // The context, a global context, can be used in the data section or apis\n}\n\n```\n### More examples:\nhttps://github.com/marvelbark2/ryo-js-examples\n\n## Primary deps:\n\n- Esbuild\n- Babel\n- uwebSockets.js\n- Flamethrower\n\n## Thanks to:\n- https://github.com/lydiahallie/byof-demo\n\nFill free to add PRs or issues","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarvelbark2%2Fryo-js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarvelbark2%2Fryo-js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarvelbark2%2Fryo-js/lists"}