kwhitley / itty-router

A little router.
MIT License
1.78k stars 78 forks source link

Working nested routes without base. #258

Open kethan opened 2 months ago

kethan commented 2 months ago
const
    run = (...fns) => async (...args) => {
        for (const fn of fns.flat(1 / 0)) {
            const result = fn && await fn(...args);
            if (result !== undefined) return result;
        }
    },
    compile = (path) => RegExp(`^${path
        .replace(/\/+(\/|$)/g, '$1')                       // strip double & trailing splash
        .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))')       // greedy params
        .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))')  // named params and image format
        .replace(/\./g, '\\.')                              // dot in path
        .replace(/(\/?)\*/g, '($1.*)?')
        }/*$`),
    mount = (fn) => fn.fetch || fn,
    lead = x => x.startsWith('/') ? x : '/' + x,
    add = (routes, method, base, route, handlers, path) =>
        routes.push([method, compile(base + route), handlers.map(mount), base + path]),
    use = (routes, base, route, handlers) =>
        route === "/" ?
            add(routes, "ALL", base, "/", handlers, "/") :
            route?.call || route?.fetch ?
                add(routes, "ALL", base, '/*', [route, ...handlers], "/*") :
                handlers.forEach(handler =>
                    handler?.routes?.forEach(([method, , handles, path]) =>
                        add(routes, method, base, lead(route + path), handles, lead(route + path))));
export const Router = ({ base = '', routes = [], ...other } = {}) => ({
    __proto__: new Proxy({}, {
        get: (_, prop, receiver) => (route, ...handlers) =>
            (prop.toUpperCase() === "USE" ?
        use(routes, base, route, handlers) :
        add(routes, prop.toUpperCase?.(), base, route, handlers, route),
        receiver)
    }),
    routes,
    ...other,
    async fetch(request, ...args) {
        let url = new URL(request.url), match, res, query = request.query = { __proto__: null };
        for (const [k, v] of url.searchParams) query[k] = query[k] ? ([]).concat(query[k], v) : v;
        const r = async (hns, ...params) =>
            run(hns)(...params)
                .catch((e) => other.catch ? (res = other.catch(e, request.proxy ?? request, ...args)) :
                    Promise.reject(e));
       res = await r(other.before, request.proxy ?? request, ...args);
        if (!res) {
            for (const [method, route, handlers, _] of routes) {
                if ((method === request.method || method === "ALL") && (match = url.pathname.match(route))) {
                    request.params = match.groups || {};
                    request.route = _;
                    if ((res = await r(handlers, request.proxy ?? request, ...args)) !== undefined) break;
                }
            }
        }
        return await r(other.finally, res, request.proxy ?? request, ...args) ?? res;
    }
});
const child = Router().get('*', (req) => req.params.bar)
const parent = Router()
    .get('/', () => 'parent')
    .use('/child/:bar', child)

parent.fetch({
    url: '/',
    method: 'GET'
})
    .then(console.log)
    .catch(console.error);

parent.fetch({
    url: '/child/kitten',
    method: 'GET'
})
    .then(console.log)
    .catch(console.error);
kethan commented 2 months ago

Another deeply nested example.

app.js

import { Router } from "./itty";
import sub from './sub';

const app = Router();
app.use((req) => {
    console.log("root!");
});

app.use("sub", sub);
app.get("/books", (req) => {
    return ([
        {
            id: 1,
            title: "Book 1",
            author: "Author 1",
        },
    ]);
});

app
    .fetch({
        url: "/sub/sub",
        method: "GET",
    })
    .then(console.log)
    .catch(console.error);

sub.js

import { Router } from "./itty";
import subsub from './subsub';

const app = Router();
app.use("/", (req) => {
    console.log('sub root!');
})
app.use("sub", subsub)
app.get("/", (req) => {
    return ("sub /get");
});

export default app;

subsub.js

import { Router } from "./itty";

const app = Router();
app.use("/", (req) => {
    console.log('subsub root!');
})
    .get("/", (req) => {
        return ("subsub /get");
    });

export default app;
kethan commented 2 months ago

https://github.com/kethan/nitty-router

kwhitley commented 2 months ago

Thanks @kethan - I'll take a look at this! It looks like a pretty clean implementation, but I'll have to weigh the bytes... at a glance, this looks like it adds quite a chunk to the final size...