Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
latest
ngasull/classicThis package works with DenoIt is unknown whether this package works with Cloudflare Workers, Node.js, Bun
JSR Score
41%
Published
2 months ago (0.1.1)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358import { type JSable, type ServedJSContext } from "jsr:@classic/js@0"; import { accepts } from "jsr:@std/http@^0.224.5"; import { Fragment, jsx } from "./jsx-runtime.ts"; import { $client, $effects, $served, createContext, Html, initContext, render, } from "./render.ts"; import type { JSX, JSXComponent, JSXContextAPI, JSXContextInit, } from "./types.ts"; type LayoutComponent<Params extends string> = JSXComponent< { [P in Params]: string } & { readonly children?: JSX.Element } >; type PartComponent<Params extends string> = JSXComponent< { [P in Params]: string } >; type Action<PC extends PartComponent<string>> = unknown; type RoutedRequest<Params extends string> = { readonly req: Request; readonly params: { [P in Params]: string }; readonly use: JSXContextAPI; }; type Handler<Params extends string> = ( req: RoutedRequest<Params>, ) => JSX.Element | Response | void | PromiseLike<JSX.Element | Response | void>; type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; export const route = <T extends string = never>(): Segment<T, T, undefined> => new Segment(); export type { Segment }; class Segment< ParentParams extends Params, Params extends string, PComponent extends PartComponent<Params> | undefined, > { #Layout?: LayoutComponent<ParentParams>; #Part?: PComponent; #Action?: PComponent extends PartComponent<Params> ? Action<PComponent> : never; #apiHandlers: Map<Method, Handler<Params>> = new Map(); #segments: Record< string, ( // deno-lint-ignore no-explicit-any | Segment<any, any, any> | (( // deno-lint-ignore no-explicit-any segment: Segment<any, any, any>, // deno-lint-ignore no-explicit-any ) => Segment<any, any, any> | PromiseLike<Segment<any, any, any>>) )[] > = {}; #param?: string; route< P extends string, SubSegment extends Segment< Params, Params | (P extends `:${infer Param}` ? Param : never), any >, >( segment: P, sub: | SubSegment | ((segment: SubSegment) => SubSegment | PromiseLike<SubSegment>), ): Segment<ParentParams, Params, PComponent> { if (!segment) throw Error(`Route segment name can't be empty`); const subs = this.#segments[segment] ??= []; // @ts-ignore dynamism is protected by `segment` signature if (!subs.includes(sub)) subs.push(sub); const wildMatch = segment.match(wildcardRegExp); if (wildMatch) { if (this.#param) { throw Error( `The same segment doesn't allow multiple wildcard sub-routes`, ); } this.#param = wildMatch[1] ?? "*"; } return this; } layout(Layout: LayoutComponent<ParentParams>): this; layout(): LayoutComponent<ParentParams> | undefined; layout(Layout?: LayoutComponent<ParentParams>) { if (!Layout) return this.#Layout; if (this.#Layout) throw Error(`Layout is already set`); this.#Layout = Layout; return this; } part<PC extends PartComponent<Params>>( Part: PC, ): Segment<ParentParams, Params, PC>; part(): PComponent; part(Part?: PartComponent<Params>) { if (!Part) return this.#Part; if (this.#Part) throw Error(`Part is already set`); this.#Part = Part as PComponent; return this as Segment<ParentParams, Params, PartComponent<Params>>; } action( Action: PComponent extends PartComponent<Params> ? Action<PComponent> : never, ): PComponent extends PartComponent<Params> ? this : never; action(): PComponent extends PartComponent<Params> ? Action<PComponent> | undefined : never; action( Action?: PComponent extends PartComponent<Params> ? Action<PComponent> : never, ) { if (!Action) return this.#Action; if (this.#Action) throw Error(`Action is already set`); this.#Action = Action; return this; } api<H extends Handler<Params> | undefined>( method: Method, handler?: H, ): undefined extends H ? Handler<Params> | undefined : this { if (!handler) return this.#apiHandlers.get(method) as any; if (this.#apiHandlers.has(method)) throw Error(`${method} is already set`); this.#apiHandlers.set(method, handler); return this as any; } async #matchRoutes( [candidate, ...nextSegments]: string[], parentSegment: string = "", parentParams: Record<ParentParams, string> = {} as Record< ParentParams, string >, params: Record<Params, string> = parentParams as Record<Params, string>, ): Promise< | void | (readonly [ Segment<ParentParams, Params, PComponent>, string, Record<ParentParams, string>, Record<Params, string>, ])[] > { if (candidate) { if (this.#segments[candidate]) { let i = -1; for (let subRouter of this.#segments[candidate]) { i++; if (!(subRouter instanceof Segment)) { subRouter = await subRouter(new Segment()); this.#segments[candidate][i] = subRouter; } const match = await subRouter.#matchRoutes( nextSegments, candidate, params, params, ); if (match) { match.unshift([this, parentSegment, parentParams, params]); return match; } } } if (this.#param) { const wildcard = this.#param === "*" ? this.#param : `:${this.#param}`; const nextParams = { ...params, [this.#param]: candidate }; let i = -1; for (let subRouter of this.#segments[wildcard]) { i++; if (!(subRouter instanceof Segment)) { subRouter = await subRouter(new Segment()); this.#segments[wildcard][i] = subRouter; } const match = await subRouter.#matchRoutes( nextSegments, wildcard, params, nextParams, ); if (match) { match.unshift([this, parentSegment, parentParams, params]); return match; } } } } else { return [[this, parentSegment, parentParams, params]]; } } async fetch( req: Request, { context, js: jsContext }: { context?: JSXContextInit<unknown>[] | JSXContextAPI | undefined; js: ServedJSContext; }, ): Promise<Response | void> { const use = initContext(context); use.provide($initResponse, {}); use.provide($served, jsContext); const acceptsHtml = req.method === "GET" && accepts(req).includes("text/html"); const { pathname, searchParams } = new URL(req.url); const segments = pathname === "/" ? [] : pathname.slice(1).split("/"); const layouts = await this.#matchRoutes(segments); if (!layouts) return; const [lastSegment, , , partParams] = layouts[layouts.length - 1]; const reqFrom = req.method === "GET" && searchParams.get("cc-from")?.split("/"); if (reqFrom || acceptsHtml) { const part = jsx(lastSegment.part() ?? NotFound, partParams); let resFromIndex = 0; if (reqFrom) { for ( ; resFromIndex < reqFrom.length && resFromIndex + 1 < layouts.length && reqFrom[resFromIndex] === layouts[resFromIndex + 1][1]; resFromIndex++ ); } const segments = reqFrom ? layouts.slice(resFromIndex + 1) : layouts; let stream = render( jsx("cc-route", { children: jsx(Html, { contents: render(part, { context: initContext(use) }), }), }), ); for (let i = segments.length - 1; i >= 0; i--) { const [segment, path, layoutParams] = segments[i]; const laidout = jsx(segment.layout() ?? Fragment, { ...layoutParams, children: jsx(Html, { contents: stream }), }); const context = initContext(use); if (i === 0 && !reqFrom) { context.provide($effects, [ use( $client<typeof import("./client-router.ts")>( "@classic/server/client/router", ), ).init() as JSable<void>, ]); } stream = render( path ? reqFrom && i === 0 ? jsx("html", { children: jsx("body", { children: layout( path, layoutParams, segment.layout(), stream, ), }), }) : layout(path, layoutParams, segment.layout(), stream) : laidout, { context }, ); } const { status, headers = {} } = use($initResponse); headers["Content-Type"] = "text/html; charset=UTF-8"; if (reqFrom) { headers["CC-From"] = reqFrom.slice(0, resFromIndex).join("/"); } return new Response(stream, { status, headers }); } else { const lastSegmentHandler = lastSegment.api(req.method as Method); const handlerRes = lastSegmentHandler && await lastSegmentHandler({ req, params: partParams, use, }); if (handlerRes) { if (handlerRes instanceof Response) { return handlerRes; } } } } } const layout = <Params extends string>( path: string | undefined, params: Record<Params, string>, Layout: LayoutComponent<Params> = Fragment, stream: ReadableStream<Uint8Array>, ) => jsx("cc-route", { path, children: [ jsx(Layout, { ...params, children: jsx("slot") }), jsx(Html, { contents: stream }), ], }); const $initResponse = createContext<{ status?: number; headers?: Record<string, string>; }>("initResponse"); export const $send = ( use: JSXContextAPI, opts: { status?: number; headers?: Record<string, string> }, ): void => { Object.assign(use($initResponse), opts); }; const wildcardRegExp = /^(?:\*|:(.*))$/; const NotFound: JSXComponent = (_, use) => { use($send, { status: 404 }); return Fragment({ children: ["Not found"] }); };