
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
Ryo-js is a fullstack framework for building universal web applications with preact and graphql.
Small js fullstack framework blazly fast Memo version
npm i ryo.js #or npm i ryo.js@github:marvelbark2/ryo-js
Routing system: Based on filesystem, you can build dynamic route, naming file with ":" prefix
Preact Components:
Static Component (Sync data fetching): export data method returning a value
Static Component (Async data fetching): export data object contains:
Server Component (TODO): export server method without returning anything
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)
Api: export function with method name, like: export get() { return ... }
Websockets: naming the file in src folder with ".ws.js" suffix:
Server-Sent-Events: naming the file in src folder with ".ev.js" suffix:
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)
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 ...)
Security headers (2 complete): You can handle authentification and authorization by using ryo.config.js.
Example with jwt authentification and subdomain for blog project:
// file: ryo.config.js
/** @type {import('ryo.js').RyoConfig} */
const bcrypt = require("bcrypt");
const jsonwebtoken = require("jsonwebtoken");
const BASE_HOST = process.env.BASE_HOST || "localhost:3000";
const jwtAuthFilter = {
doFilter(request, _response, setAuthContext, next) {
const authHeader = request.getHeader("authorization");
if (authHeader.length === 0 || !authHeader.startsWith("Bearer ")) {
return next();
}
const jwt = authHeader.substring(7);
const decoded = jsonwebtoken.verify(jwt, "secret");
setAuthContext({
id: decoded.username,
roles: decoded.roles,
})
return next();
}
}
module.exports = {
subdomain: {
baseHost: BASE_HOST,
},
security: {
cors: false,
csrf: true,
authorizeHttpRequests: [
{
path: ["/", "/*.{js,css}", "/images/*", "/blog", "/_subdomain/test/**/*.{js,css}", "/_subdomain/**/test"],
status: "allow",
},
{
path: ["/blog/ff", "/_subdomain/test/page"],
status: "auth",
},
{
path: "/_subdomain/test/",
roles: ["admin"],
}
],
authProvider: {
async loadUserByUsername(username) {
return {
username,
plainTextPassword: bcrypt.hashSync("123456", 10),
roles: [username],
onLoginSuccess: (res) => {
const jwt = jsonwebtoken.sign({
username,
roles: [username]
}, "secret");
console.log({
jwt, res
});
res.writeHeader("Authorization", `Bearer ${jwt}`);
res.end("Done")
}
}
},
passwordEncoder: {
encode: async (password) => bcrypt.hash(password, 10),
matches: async (password, hash) => bcrypt.compare(password, hash),
}
},
sessionManagement: {
sessionCreationPolicy: "stateless",
},
filter: [
jwtAuthFilter
]
},
}
JSON api
Readable stream api
API tools
// Path: src/msg.ws.js
export default {
open: (ws, req) => {
console.log("NEW CLIENT on /msg");
},
message: (ws, message, isBinary) => { },
close: (ws, code, message) => {
}
}
// Path: src/api.js
export function get({ url }) {
return {
message: "Hello from " + url
};
}
//body: object(json input) | Buffer(buffer array input) | undefined(none)
export function post({ body }) {
// do something using body
console.log({ body });
return {
message: "Hello from " + (typeof body)
};
}
// Path: src/file.js
import fs from 'fs';
import { join } from 'path';
export function get({ url }) {
const path = join(process.cwd(), "./screen.mov");
const stream = fs.createReadStream(path)
stream.on("error", () => {
//Handle error to avoid server crash
console.log("error");
})
const length = fs.statSync(path).size;
return {
stream, length
};
}
// path: ttql.gql.ts
export default {
schema: `
type Query {
hello: String
}
type Mutation {
capitalize(message: String): String
}
`,
resolvers: {
hello: (_: unknown, ctx: { test: string }) => `${ctx.test}: hello world`,
capitalize: ({ message }: { message: string }) => message.toUpperCase()
},
context: {
test: "ME"
}
}
use NODE_ENV=development to access graphql playground in GET request as example: /ttql.gql
import { PubSub } from 'graphql-subscriptions'
const TODOS_CHANNEL = "TODOS_CHANNEL";
const pubsub = new PubSub();
const todos = [
{
id: "1",
text: "Learn GraphQL + Soild",
done: false,
},
];
const typeDefs = `
type Todo {
id: ID!
done: Boolean!
text: String!
}
type Query {
getTodos: [Todo]!
}
type Mutation {
addTodo(text: String!): Todo
setDone(id: ID!, done: Boolean!): Todo
}
type Subscription {
todos: [Todo]!
}
`;
const resolvers = {
getTodos: () => {
return todos;
},
addTodo: (
{ text }: { text: string },
{ pubsub }: { pubsub: PubSub }
) => {
const newTodo = {
id: String(todos.length + 1),
text,
done: false,
};
todos.push(newTodo);
pubsub.publish(TODOS_CHANNEL, { todos });
return newTodo;
},
setDone: (
{ id, done }: { id: string; done: boolean },
{ pubsub }: { pubsub: PubSub }
) => {
const todo = todos.find((todo) => todo.id === id);
if (!todo) {
throw new Error("Todo not found");
}
todo.done = done;
pubsub.publish(TODOS_CHANNEL, { todos });
return todo;
},
todos: (_: unknown, { pubsub }: { pubsub: PubSub }) => {
const iterator = pubsub.asyncIterator(TODOS_CHANNEL);
pubsub.publish(TODOS_CHANNEL, { todos });
return iterator;
},
}
export default {
schema: typeDefs,
resolvers: resolvers,
context: {
pubsub
}
}
//Path: src/server.jsx
export function server({ req }) {
return {
status: 201,
headers: {
"X-TEST": "YES",
},
body: {
"From": "SERVER",
}
}
}
export default function index({ data }) {
return (
<div>
<h1>Server Side Rendering <b></b></h1>
<p>From: {data.From}</p>
</div>
)
}
// Path: src/index.jsx
// route: /
import { useEffect, useState } from "react";
// Server side function
export function data() {
return {
"counter": 3,
}
}
export default function index({ data }) {
const [count, setCount] = useState(data.counter);
useEffect(() => {
window.addEventListener('flamethrower:router:fetch-progress', ({ detail }) => {
console.log('Fetch Progress:', detail);
});
}, [])
return (
<div className="w-screen h-screen flex items-center justify-center bg-gray-50">
<div className="flex flex-col w-full p-10 mx-24 border border-dashed border-gray-500 space-y-6 items-center">
<p>You clicked <span className="font-bold text-lg text-gray-800">{count}</span> times</p>
<button className="bg-blue-50 p-3 border-blue-700 text-blue-700 w-24 rounded-xl" onClick={() => setCount(count + 1)}>Click me</button>
{/* SPA routing thanks to: Flamethrower */}
<a href="/data">Data</a>
<a href="/api">TEST</a>
</div>
</div>
)
}
// Path: src/counter.jsx
// route: /counter
type CounterDataType = { value: number, date: Date };
let count = 0;
export const data = {
invalidate: 1,
shouldUpdate: (_old: CounterDataType, newValue: CounterDataType) => newValue.value > 10,
runner: async (stop: () => void, old?: CounterDataType) => {
if (old?.count === 60) {
stop();
}
return {
value: count++,
date: new Date()
};
}
}
// Parent Layout
export function Parent({ children }: { children: any }) {
return (
<div>
<h1>Parent</h1>
{children}
</div>
)
}
export default function index({ data }: { data: CounterDataType }) {
return (
<div>
<p>
COUNTING at {data.date.getTime()} ... {data.value}
</p>
</div>
)
}
//path: src/data.jsx
//route: /data
// Other example & router
import { PrismaClient } from '@prisma/client'
import { useEffect } from 'react';
import Btn from '../comp/Btn';
import Router from '../lib/router/router';
export const data = {
invalidate: 1000,
runner: async (stop) => {
const prisma = new PrismaClient();
const events = await prisma.event.findMany({});
return { events };
}
}
export default function index({ data }) {
const router = Router();
useEffect(() => {
console.log({ data });
}, [])
if (router.isLoading) return <div>Loading...</div>
return (
<div>
<p>
<span onClick={() => router.back()}>BACK</span>
<Btn text="TEST" />
{
data.events.map((event) => {
return (
<div key={event.id}>
<a href={`/blog/${event.id}`}>{event.name}</a>
</div>
)
})
}
</p>
</div>
)
}
import Articles from "@/components/Articles";
import type { Article } from "@/types";
import type { RyoDataObject } from "ryo.js";
export const data: RyoDataObject = {
source: {
// root path is CWD
file: "data/articles.json",
},
invalidate: 20,
shouldUpdate: () => true
}
export default function App({ data }: { data: Article[] }) {
return (
<Articles articles={data} />
)
}
//path: src/blog/:id.jsx
//route: /blog/ID
import { useEffect } from "react";
import Router from "ryo.js/router"
export default function index({ ...props }) {
const router = Router();
if (router.isLoading) return <div>Loading...</div>
return (
<div>
Blog id: {router.query.id}
<span >Return Back</span>
</div>
)
}
//path: src/events/:id.ev.js
//route: /events/ID.ev
export default {
invalidate: 1000,
runner: async ({ params }) => {
console.log(params)
return { message: "I'm the user: " + params.id };
}
}
in 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)
import { useEffect, useState } from "react";
import { test } from "@lib/db-exec";
import toast, { Toaster } from 'react-hot-toast';
function data() {
test()
//test()
return {
"counter": 3,
saveOnData(data: any) {
console.log(data)
}
}
}
// Offline component
export function offline() {
const [counter, setCounter] = useState(0);
return (
<div className="bg-gray-50">
<Toaster
position="top-right"
toastOptions={{
duration: 10000,
}}
/>
<button className="px-4 py-2 bg-blue-900 text-white rounded-2xl" onClick={() => toast.success("Good")} >Toast</button>
<button className="mx-3 px-4 py-2 text-blue-900 border-blue-900 border bg-white rounded-2xl"
onClick={() => setCounter(c => c + 1)}
>
counting <b>{counter}</b>
</button>
</div>
)
}
export default function index({ data }: { data: { counter: number, saveOnData: (data: any) => void } }) {
const [count, setCount] = useState(data.counter);
useEffect(() => {
console.log("Hello from client side: ", data)
}, [data]);
return (
<div className="w-screen h-screen flex items-center justify-center bg-gray-50">
<Toaster
position="top-right"
toastOptions={{
duration: 10000,
}}
/>
<div className="flex flex-col w-full p-10 mx-24 border border-dashed border-gray-500 space-y-6 items-center">
<p>You clicked <span className="font-bold text-lg text-gray-800">{count}</span> times</p>
<button className="bg-blue-50 p-3 border-blue-700 text-blue-700 w-24 rounded-xl" onClick={() => setCount(count + 1)}>Click me</button>
<span className="hover:cursor-pointer" onClick={() => toast.success('HOL')}>TOAST click</span>
{/* SPA routing thanks to: Flamethrower */}
<a href="/data">Data</a>
<a href="/api">TEST</a>
<button onClick={() => data.saveOnData({ a: "me" })}>API TEST</button>
</div>
</div>
)
}
export {
data
}
You can add middleware by adding a file in root of the project: middleware.(ts|js) You 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
//path: middleware.js
const getCookie = (req, name) =>
(req.cookies ??= req.getHeader('cookie')).match(getCookie[name] ??= new RegExp(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`))?.[2]
export default function middleware(req, res, next, error) {
const user = getCookie(req, "user")
if (user) {
return next();
} else {
res.writeHeader("Set-Cookie", "user=Guest")
return res.writeStatus("401").end("You are not authorized")
}
}
//Optional
export function init({ context }: { context: Map<string, any> }) {
const db = ... // Inmemory database or other stuff
context.set("articleDb", db); // The context, a global context, can be used in the data section or apis
}
https://github.com/marvelbark2/ryo-js-examples
Fill free to add PRs or issues
FAQs
Ryo-js is a fullstack framework for building universal web applications with preact and graphql.
The npm package ryo.js receives a total of 0 weekly downloads. As such, ryo.js popularity was classified as not popular.
We found that ryo.js demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.