JSPM

  • Created
  • Published
  • Downloads 1
  • Score
    100M100P100Q71121F
  • License ISC

Ryo-js is a fullstack framework for building universal web applications with preact and graphql.

Package Exports

  • ryo.js
  • ryo.js/event-signal
  • ryo.js/package.json
  • ryo.js/router

Readme

Ryo js

Small js fullstack framework blazly fast Memo version

Installation

npm i ryo.js #or npm i ryo.js@github:marvelbark2/ryo-js

Features:

  • Routing based filesystem
  • Blazly fast (Try it by yourself)
  • Everything on src folder
  • Create apis, websockets, graphQL, Server-Sent-Events (SSE), preact components and serve static files.
  • SPA routing which makes the site so fast (Using Flamethrower)
  • 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)

what you can do with 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:
      • runner: Function async accepts stop method as argument (stop: called to stop caching) returns a value
      • invalidate(Optional): Field, duration per second to cache value, it's a global value.
      • shouldUpdate(Optional): Function accepts two values (Old, new) to re-render component on runtime when data changed after the cache invalidated
    • Server Component (TODO): export server method without returning anything
      • 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)
    • 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 ... }

    • JSON api: By returning js objects parsable values
    • Streamable api: By returning object:
      • stream: Created stream, like readStream
      • length: Stream length (without reading it)
    • 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
    • GraphQL endpoints (Still fixing subscriptions): You can build many graphql endpoint with separated schema by naming the route with this extension .gql.(ts | js)
  • Websockets: naming the file in src folder with ".ws.js" suffix:

    • Return object match uWebSockets.js documentation
  • Server-Sent-Events: naming the file in src folder with ".ev.js" suffix:

    • Export default: object with invalidate field (ms) and runner function (Async with params route if needed)

Progress Status:

  • Preact Components
    • Async static component
    • Sync static component
    • Server Component
    • Server Component with hooks
  • Api
    • JSON api
    • Readable stream api
  • GraphQL
    • Query
    • Mutation
    • Subscription
  • Websockets
  • Server-Sent-Events

Example:

websockets:

// Path: src/msg.ws.js
export default {
    open: (ws, req) => {
        console.log("NEW CLIENT on /msg");
    },

    message: (ws, message, isBinary) => { },

    close: (ws, code, message) => {

    }
}

API

JSON API:

// 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)
    };
}

Streamable API:

// 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
    };
}

GraphQL endpoint:

// 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

Preact components:

Server components:

At least for now: ** You can't use hooks **

//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</h1>

            <p>From: {data.From}</p>
        </div>
    )
}

Static sync component:

// 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>
    )
}

Static async/fresh component:

// 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>
    )
}

Dynamic route (component):

//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 };
    }
}

More examples:

https://github.com/marvelbark2/ryo-js-examples

Primary deps:

  • Esbuild
  • Babel
  • uwebSockets.js
  • Flamethrower

Thanks to:

Fill free to add PRs or issues