@papack/ssr 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ let e=require(`node:http`);function t(e){let{each:t,children:n}=e;if(!t||t.length===0)return null;let r=n[0];if(typeof r!=`function`)throw Error(`<For> expects a single function child`);return t.map(e=>r(e))}async function n(e){let{children:t}=e;return t?(await Promise.all(t)).join(``):``}async function r(e,t,...n){if(typeof e==`function`)return await e({...t??{},children:n});let r=`<${e}`;if(t)for(let[e,n]of Object.entries(t))e!==`children`&&(n==null||n===!1||(n===!0?r+=` ${e}`:r+=` ${e}="${String(n)}"`));r+=`>`;for(let e of n)r+=await i(e);return r+=`</${e}>`,r}async function i(e){return e==null||e===!1||e===!0?``:e instanceof Promise?i(await e):Array.isArray(e)?(await Promise.all(e.map(i))).join(``):String(e)}var a=class{server;routes=[];ctx;notFoundHandler;errorHandler;constructor(t){this.ctx=t,this.server=(0,e.createServer)(async(e,t)=>{try{if(e.method!==`GET`){t.statusCode=405,t.end(`METHOD_NOT_ALLOWED`);return}let n=e.url??`/`,r=new URL(n,`http://localhost`),i=this.splitPath(r.pathname);for(let n of this.routes){if(n.parts.length!==i.length)continue;let a={},o=!0;for(let e=0;e<n.parts.length;e++){let t=n.parts[e],r=i[e];if(!t||!r){o=!1;break}if(t.startsWith(`:`))a[t.slice(1)]=r;else if(t!==r){o=!1;break}}if(!o)continue;let s=r.pathname+`?`+Object.entries(a).map(([e,t])=>`${e}=${t}`).join(`&`);if(n.ttl&&n.cache){let e=n.cache.get(s);if(e&&e.expires>Date.now()){t.statusCode=200,t.setHeader(`content-type`,n.contentType),t.end(e.value);return}}let c={req:e,res:t,params:a,...this.ctx},l=await n.handler(c);n.contentType.startsWith(`text/html`)&&(l=`<!DOCTYPE html>`+l),n.ttl&&n.cache&&n.cache.set(s,{value:l,expires:Date.now()+n.ttl}),t.statusCode=200,t.setHeader(`content-type`,n.contentType),t.end(l);return}if(this.notFoundHandler){let n={req:e,res:t,params:{},...this.ctx},r=await this.notFoundHandler(n);t.statusCode=404,t.setHeader(`content-type`,`text/html; charset=utf-8`),t.end(r);return}t.statusCode=404,t.end(`NOT_FOUND`)}catch(n){await this.handleError(n,e,t)}})}async handleError(e,t,n){if(this.errorHandler){let r={req:t,res:n,params:{},...this.ctx},i=await this.errorHandler(r,e);n.statusCode=500,n.setHeader(`content-type`,`text/html; charset=utf-8`),n.end(i);return}n.statusCode=500,n.setHeader(`content-type`,`text/plain`),n.end(`INTERNAL_ERROR`)}html(e,t){this.addRoute(e.path,`text/html; charset=utf-8`,t,e.ttl)}css(e,t){this.addRoute(e.path,`text/css; charset=utf-8`,t,e.ttl)}js(e,t){this.addRoute(e.path,`application/javascript; charset=utf-8`,t,e.ttl)}notFound(e){this.notFoundHandler=e}error(e){this.errorHandler=e}listen(e,t){this.server.listen(e,t)}addRoute(e,t,n,r){let i=this.splitPath(e);this.routes.push({parts:i,contentType:t,handler:n,ttl:r,cache:r?new Map:void 0})}splitPath(e){return e.split(`/`).filter(Boolean)}};async function o(e){return e.when!==!0||!e.children||e.children.length===0?``:(await Promise.all(e.children)).join(``)}exports.For=t,exports.Router=a,exports.Show=o,exports.fragment=n,exports.jsx=r;
@@ -0,0 +1,65 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ //#region core/for.d.ts
4
+ type ForProps<T> = {
5
+ each: readonly T[];
6
+ children: [(item: T) => any];
7
+ };
8
+ declare function For<T>(props: ForProps<T>): any[] | null;
9
+ //#endregion
10
+ //#region core/jsx.d.ts
11
+ declare global {
12
+ namespace JSX {
13
+ type Element = string | Promise<string>;
14
+ interface IntrinsicElements {
15
+ [tag: string]: any;
16
+ }
17
+ }
18
+ }
19
+ type Primitive = string | number | boolean | null | undefined;
20
+ type Child = Primitive | Child[] | Promise<Primitive | Child[]>;
21
+ type Props = {
22
+ [key: string]: any;
23
+ children?: Child[];
24
+ };
25
+ declare function jsx(tag: string | ((props: any) => string | Promise<string>), props: Props | null, ...children: Child[]): Promise<string>;
26
+ //#endregion
27
+ //#region core/fragment.d.ts
28
+ declare function fragment(props: Props): Promise<string>;
29
+ //#endregion
30
+ //#region core/router.d.ts
31
+ type Context<Ctx> = {
32
+ req: IncomingMessage;
33
+ res: ServerResponse;
34
+ params: Record<string, string>;
35
+ } & Ctx;
36
+ type RouteOptions = {
37
+ path: string;
38
+ ttl?: number;
39
+ };
40
+ declare class Router<Ctx extends object> {
41
+ private server;
42
+ private routes;
43
+ private ctx;
44
+ private notFoundHandler?;
45
+ private errorHandler?;
46
+ constructor(ctx: Ctx);
47
+ private handleError;
48
+ html(opts: RouteOptions, handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
49
+ css(opts: RouteOptions, handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
50
+ js(opts: RouteOptions, handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
51
+ notFound(handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
52
+ error(handler: (ctx: Context<Ctx>, err: unknown) => string | Promise<string>): void;
53
+ listen(port: number, cb?: () => void): void;
54
+ private addRoute;
55
+ private splitPath;
56
+ }
57
+ //#endregion
58
+ //#region core/show.d.ts
59
+ type ShowProps = {
60
+ when: boolean;
61
+ children: any;
62
+ };
63
+ declare function Show(p: ShowProps): Promise<string>;
64
+ //#endregion
65
+ export { Context, For, Props, Router, Show, fragment, jsx };
@@ -0,0 +1,66 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ //#region core/for.d.ts
4
+ type ForProps<T> = {
5
+ each: readonly T[];
6
+ children: [(item: T) => any];
7
+ };
8
+ declare function For<T>(props: ForProps<T>): any[] | null;
9
+ //#endregion
10
+ //#region core/jsx.d.ts
11
+ declare global {
12
+ namespace JSX {
13
+ type Element = string | Promise<string>;
14
+ interface IntrinsicElements {
15
+ [tag: string]: any;
16
+ }
17
+ }
18
+ }
19
+ type Primitive = string | number | boolean | null | undefined;
20
+ type Child = Primitive | Child[] | Promise<Primitive | Child[]>;
21
+ type Props = {
22
+ [key: string]: any;
23
+ children?: Child[];
24
+ };
25
+ declare function jsx(tag: string | ((props: any) => string | Promise<string>), props: Props | null, ...children: Child[]): Promise<string>;
26
+ //#endregion
27
+ //#region core/fragment.d.ts
28
+ declare function fragment(props: Props): Promise<string>;
29
+ //#endregion
30
+ //#region core/router.d.ts
31
+ type Context<Ctx> = {
32
+ req: IncomingMessage;
33
+ res: ServerResponse;
34
+ params: Record<string, string>;
35
+ } & Ctx;
36
+ type RouteOptions = {
37
+ path: string;
38
+ ttl?: number;
39
+ };
40
+ declare class Router<Ctx extends object> {
41
+ private server;
42
+ private routes;
43
+ private ctx;
44
+ private notFoundHandler?;
45
+ private errorHandler?;
46
+ constructor(ctx: Ctx);
47
+ private handleError;
48
+ html(opts: RouteOptions, handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
49
+ css(opts: RouteOptions, handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
50
+ js(opts: RouteOptions, handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
51
+ notFound(handler: (ctx: Context<Ctx>) => string | Promise<string>): void;
52
+ error(handler: (ctx: Context<Ctx>, err: unknown) => string | Promise<string>): void;
53
+ listen(port: number, cb?: () => void): void;
54
+ private addRoute;
55
+ private splitPath;
56
+ }
57
+ //#endregion
58
+ //#region core/show.d.ts
59
+ type ShowProps = {
60
+ when: boolean;
61
+ children: any;
62
+ };
63
+ declare function Show(p: ShowProps): Promise<string>;
64
+ //#endregion
65
+ export { Context, For, Props, Router, Show, fragment, jsx };
66
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../core/for.ts","../core/jsx.ts","../core/fragment.ts","../core/router.ts","../core/show.ts"],"sourcesContent":[],"mappings":";;;KAAK;iBACY;oBACG;;AAGJ,iBAAA,GAAc,CAAA,CAAA,CAAA,CAAA,KAAQ,EAAR,QAAQ,CAAC,CAAD,CAAA,CAAA,EAAA,GAAA,EAAA,GAAA,IAAA;;;;;4BCHV;IDFvB,UAAQ,iBAEQ,CAAA;MAGF,CAAA,GAAA,EAAA,MAAoB,CAAT,EAAA,GAAA;;;;KCGzB,SAAA;KAEA,KAAA,GAAQ,YAAY,UAAU,QAAQ,YAAY;KAE3C,KAAA,GAVuB;EAAA,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,GAAA;EAAA,QAAA,CAAA,EAYtB,KAZsB,EAAA;AAAA,CAAA;AAQ9B,iBAOiB,GAAA,CAPZ,GAAA,EAAA,MAAA,GAAA,CAAA,CAAA,KAAA,EAAA,GAAA,EAAA,GAAA,MAAA,GAQgC,OARhC,CAAA,MAAA,CAAA,CAAA,EAAA,KAAA,EASD,KATC,GAAA,IAAA,EAAA,GAAA,QAAA,EAUK,KAVL,EAAA,CAAA,EAWP,OAXO,CAAA,MAAA,CAAA;;;iBCRY,QAAA,QAAgB,QAAQ;;;KCClC;OACL;EHJF,GAAA,EGKE,cHLM;EAKG,MAAG,EGCT,MHDS,CAAA,MAAW,EAAA,MAAA,CAAQ;IGElC;KAeC,YAAA;;;;cAKQ,MFzBsB,CAAA,YAAA,MAAA,CAAA,CAAA;EAAA,QAAA,MAAA;EAAA,QAAA,MAAA;EAM9B,QAAA,GAAA;EAEA,QAAK,eAAA;EAAG,QAAA,YAAA;EAAY,WAAA,CAAA,GAAA,EE4BN,GF5BM;EAAkB,QAAA,WAAA;EAAY,IAAA,CAAA,IAAA,EEgK7C,YFhK6C,EAAA,OAAA,EAAA,CAAA,GAAA,EEiKpC,OFjKoC,CEiK5B,GFjK4B,CAAA,EAAA,GAAA,MAAA,GEiKV,OFjKU,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAApB,GAAA,CAAA,IAAA,EEuKzB,YFvKyB,EAAA,OAAA,EAAA,CAAA,GAAA,EEwKhB,OFxKgB,CEwKR,GFxKQ,CAAA,EAAA,GAAA,MAAA,GEwKU,OFxKV,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAAO,EAAA,CAAA,IAAA,EE8KhC,YF9KgC,EAAA,OAAA,EAAA,CAAA,GAAA,EE+KvB,OF/KuB,CE+Kf,GF/Ke,CAAA,EAAA,GAAA,MAAA,GE+KG,OF/KH,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAE9B,QAAK,CAAA,OAAA,EAAA,CAEJ,GAAA,EEqLa,OFrLR,CEqLgB,GFrLhB,CAAA,EAAA,GAAA,MAAA,GEqLkC,OFrLlC,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAGI,KAAA,CAAG,OAAA,EAAA,CAAA,GAAA,EEuLN,OFvLM,CEuLE,GFvLF,CAAA,EAAA,GAAA,EAAA,OAAA,EAAA,GAAA,MAAA,GEuLkC,OFvLlC,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EACiB,MAAA,CAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,EAAA,GAAA,GAAA,IAAA,CAAA,EAAA,IAAA;EACjC,QAAA,QAAA;EACM,QAAA,SAAA;;;;KGpBV,SAAA;;;;AJKW,iBIAM,IAAA,CJAQ,CAAA,EIAA,SJAQ,CAAA,EIAI,OJAJ,CAAA,MAAA,CAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{createServer as e}from"node:http";function t(e){let{each:t,children:n}=e;if(!t||t.length===0)return null;let r=n[0];if(typeof r!=`function`)throw Error(`<For> expects a single function child`);return t.map(e=>r(e))}async function n(e){let{children:t}=e;return t?(await Promise.all(t)).join(``):``}async function r(e,t,...n){if(typeof e==`function`)return await e({...t??{},children:n});let r=`<${e}`;if(t)for(let[e,n]of Object.entries(t))e!==`children`&&(n==null||n===!1||(n===!0?r+=` ${e}`:r+=` ${e}="${String(n)}"`));r+=`>`;for(let e of n)r+=await i(e);return r+=`</${e}>`,r}async function i(e){return e==null||e===!1||e===!0?``:e instanceof Promise?i(await e):Array.isArray(e)?(await Promise.all(e.map(i))).join(``):String(e)}var a=class{server;routes=[];ctx;notFoundHandler;errorHandler;constructor(t){this.ctx=t,this.server=e(async(e,t)=>{try{if(e.method!==`GET`){t.statusCode=405,t.end(`METHOD_NOT_ALLOWED`);return}let n=e.url??`/`,r=new URL(n,`http://localhost`),i=this.splitPath(r.pathname);for(let n of this.routes){if(n.parts.length!==i.length)continue;let a={},o=!0;for(let e=0;e<n.parts.length;e++){let t=n.parts[e],r=i[e];if(!t||!r){o=!1;break}if(t.startsWith(`:`))a[t.slice(1)]=r;else if(t!==r){o=!1;break}}if(!o)continue;let s=r.pathname+`?`+Object.entries(a).map(([e,t])=>`${e}=${t}`).join(`&`);if(n.ttl&&n.cache){let e=n.cache.get(s);if(e&&e.expires>Date.now()){t.statusCode=200,t.setHeader(`content-type`,n.contentType),t.end(e.value);return}}let c={req:e,res:t,params:a,...this.ctx},l=await n.handler(c);n.contentType.startsWith(`text/html`)&&(l=`<!DOCTYPE html>`+l),n.ttl&&n.cache&&n.cache.set(s,{value:l,expires:Date.now()+n.ttl}),t.statusCode=200,t.setHeader(`content-type`,n.contentType),t.end(l);return}if(this.notFoundHandler){let n={req:e,res:t,params:{},...this.ctx},r=await this.notFoundHandler(n);t.statusCode=404,t.setHeader(`content-type`,`text/html; charset=utf-8`),t.end(r);return}t.statusCode=404,t.end(`NOT_FOUND`)}catch(n){await this.handleError(n,e,t)}})}async handleError(e,t,n){if(this.errorHandler){let r={req:t,res:n,params:{},...this.ctx},i=await this.errorHandler(r,e);n.statusCode=500,n.setHeader(`content-type`,`text/html; charset=utf-8`),n.end(i);return}n.statusCode=500,n.setHeader(`content-type`,`text/plain`),n.end(`INTERNAL_ERROR`)}html(e,t){this.addRoute(e.path,`text/html; charset=utf-8`,t,e.ttl)}css(e,t){this.addRoute(e.path,`text/css; charset=utf-8`,t,e.ttl)}js(e,t){this.addRoute(e.path,`application/javascript; charset=utf-8`,t,e.ttl)}notFound(e){this.notFoundHandler=e}error(e){this.errorHandler=e}listen(e,t){this.server.listen(e,t)}addRoute(e,t,n,r){let i=this.splitPath(e);this.routes.push({parts:i,contentType:t,handler:n,ttl:r,cache:r?new Map:void 0})}splitPath(e){return e.split(`/`).filter(Boolean)}};async function o(e){return e.when!==!0||!e.children||e.children.length===0?``:(await Promise.all(e.children)).join(``)}export{t as For,a as Router,o as Show,n as fragment,r as jsx};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["params: Record<string, string>","ctx: Context<Ctx>","ctx"],"sources":["../core/for.ts","../core/fragment.ts","../core/jsx.ts","../core/router.ts","../core/show.ts"],"sourcesContent":["type ForProps<T> = {\n each: readonly T[];\n children: [(item: T) => any];\n};\n\nexport function For<T>(props: ForProps<T>) {\n const { each, children } = props;\n if (!each || each.length === 0) return null;\n\n const render = children[0];\n if (typeof render !== \"function\") {\n throw new Error(\"<For> expects a single function child\");\n }\n\n return each.map((item) => render(item));\n}\n","import type { Props } from \"./jsx\";\n\nexport async function fragment(props: Props): Promise<string> {\n const { children } = props;\n\n if (!children) return \"\";\n\n const parts = await Promise.all(children);\n return parts.join(\"\");\n}\n","declare global {\n namespace JSX {\n type Element = string | Promise<string>;\n interface IntrinsicElements {\n [tag: string]: any;\n }\n }\n}\ntype Primitive = string | number | boolean | null | undefined;\n\ntype Child = Primitive | Child[] | Promise<Primitive | Child[]>;\n\nexport type Props = {\n [key: string]: any;\n children?: Child[];\n};\n\nexport async function jsx(\n tag: string | ((props: any) => string | Promise<string>),\n props: Props | null,\n ...children: Child[]\n): Promise<string> {\n if (typeof tag === \"function\") {\n return await tag({ ...(props ?? {}), children });\n }\n\n let html = `<${tag}`;\n\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === \"children\") continue;\n if (value == null || value === false) continue;\n\n if (value === true) {\n html += ` ${key}`;\n } else {\n html += ` ${key}=\"${String(value)}\"`;\n }\n }\n }\n\n html += \">\";\n\n for (const c of children) {\n html += await renderChild(c);\n }\n\n html += `</${tag}>`;\n\n return html;\n}\n\nasync function renderChild(child: Child): Promise<string> {\n if (child == null || child === false || child === true) return \"\";\n\n if (child instanceof Promise) {\n return renderChild(await child);\n }\n\n if (Array.isArray(child)) {\n const parts = await Promise.all(child.map(renderChild));\n return parts.join(\"\");\n }\n\n return String(child);\n}\n","import { createServer, type Server } from \"node:http\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nexport type Context<Ctx> = {\n req: IncomingMessage;\n res: ServerResponse;\n params: Record<string, string>;\n} & Ctx;\n\ntype CacheEntry = {\n value: string;\n expires: number;\n};\n\ntype Route<Ctx> = {\n parts: string[];\n contentType: string;\n ttl?: number;\n cache?: Map<string, CacheEntry>;\n handler: (ctx: Context<Ctx>) => string | Promise<string>;\n};\n\ntype RouteOptions = {\n path: string;\n ttl?: number;\n};\n\nexport class Router<Ctx extends object> {\n private server: Server;\n private routes: Route<Ctx>[] = [];\n private ctx: Ctx;\n\n private notFoundHandler?: (ctx: Context<Ctx>) => string | Promise<string>;\n private errorHandler?: (\n ctx: Context<Ctx>,\n err: unknown\n ) => string | Promise<string>;\n\n constructor(ctx: Ctx) {\n this.ctx = ctx;\n\n this.server = createServer(async (req, res) => {\n try {\n if (req.method !== \"GET\") {\n res.statusCode = 405;\n res.end(\"METHOD_NOT_ALLOWED\");\n return;\n }\n\n const rawUrl = req.url ?? \"/\";\n const url = new URL(rawUrl, \"http://localhost\");\n const pathParts = this.splitPath(url.pathname);\n\n for (const route of this.routes) {\n if (route.parts.length !== pathParts.length) continue;\n\n const params: Record<string, string> = {};\n let match = true;\n\n for (let i = 0; i < route.parts.length; i++) {\n const routePart = route.parts[i];\n const pathPart = pathParts[i];\n\n if (!routePart || !pathPart) {\n match = false;\n break;\n }\n\n if (routePart.startsWith(\":\")) {\n params[routePart.slice(1)] = pathPart;\n } else if (routePart !== pathPart) {\n match = false;\n break;\n }\n }\n\n if (!match) continue;\n\n const cacheKey =\n url.pathname +\n \"?\" +\n Object.entries(params)\n .map(([k, v]) => `${k}=${v}`)\n .join(\"&\");\n\n if (route.ttl && route.cache) {\n const hit = route.cache.get(cacheKey);\n if (hit && hit.expires > Date.now()) {\n res.statusCode = 200;\n res.setHeader(\"content-type\", route.contentType);\n res.end(hit.value);\n return;\n }\n }\n\n const ctx: Context<Ctx> = {\n req,\n res,\n params,\n ...this.ctx,\n };\n\n let body = await route.handler(ctx);\n\n if (route.contentType.startsWith(\"text/html\")) {\n body = \"<!DOCTYPE html>\" + body;\n }\n\n if (route.ttl && route.cache) {\n route.cache.set(cacheKey, {\n value: body,\n expires: Date.now() + route.ttl,\n });\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", route.contentType);\n res.end(body);\n return;\n }\n\n if (this.notFoundHandler) {\n const ctx: Context<Ctx> = {\n req,\n res,\n params: {},\n ...this.ctx,\n };\n\n const html = await this.notFoundHandler(ctx);\n res.statusCode = 404;\n res.setHeader(\"content-type\", \"text/html; charset=utf-8\");\n res.end(html);\n return;\n }\n\n res.statusCode = 404;\n res.end(\"NOT_FOUND\");\n } catch (err) {\n await this.handleError(err, req, res);\n }\n });\n }\n\n private async handleError(\n err: unknown,\n req: IncomingMessage,\n res: ServerResponse\n ) {\n if (this.errorHandler) {\n const ctx: Context<Ctx> = {\n req,\n res,\n params: {},\n ...this.ctx,\n };\n\n const html = await this.errorHandler(ctx, err);\n res.statusCode = 500;\n res.setHeader(\"content-type\", \"text/html; charset=utf-8\");\n res.end(html);\n return;\n }\n\n res.statusCode = 500;\n res.setHeader(\"content-type\", \"text/plain\");\n res.end(\"INTERNAL_ERROR\");\n }\n\n html(\n opts: RouteOptions,\n handler: (ctx: Context<Ctx>) => string | Promise<string>\n ) {\n this.addRoute(opts.path, \"text/html; charset=utf-8\", handler, opts.ttl);\n }\n\n css(\n opts: RouteOptions,\n handler: (ctx: Context<Ctx>) => string | Promise<string>\n ) {\n this.addRoute(opts.path, \"text/css; charset=utf-8\", handler, opts.ttl);\n }\n\n js(\n opts: RouteOptions,\n handler: (ctx: Context<Ctx>) => string | Promise<string>\n ) {\n this.addRoute(\n opts.path,\n \"application/javascript; charset=utf-8\",\n handler,\n opts.ttl\n );\n }\n\n notFound(handler: (ctx: Context<Ctx>) => string | Promise<string>) {\n this.notFoundHandler = handler;\n }\n\n error(\n handler: (ctx: Context<Ctx>, err: unknown) => string | Promise<string>\n ) {\n this.errorHandler = handler;\n }\n\n listen(port: number, cb?: () => void) {\n this.server.listen(port, cb);\n }\n\n private addRoute(\n path: string,\n contentType: string,\n handler: (ctx: Context<Ctx>) => string | Promise<string>,\n ttl?: number\n ) {\n const parts = this.splitPath(path);\n\n this.routes.push({\n parts,\n contentType,\n handler,\n ttl,\n cache: ttl ? new Map() : undefined,\n });\n }\n\n private splitPath(path: string): string[] {\n return path.split(\"/\").filter(Boolean);\n }\n}\n","type ShowProps = {\n when: boolean;\n children: any;\n};\n\nexport async function Show(p: ShowProps): Promise<string> {\n if (p.when !== true) return \"\";\n if (!p.children || p.children.length === 0) return \"\";\n\n const parts = await Promise.all(p.children);\n return parts.join(\"\");\n}\n"],"mappings":"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"}
package/license.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Matthias Steiner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@papack/ssr",
3
+ "description": "Minimal server-side rendering framework",
4
+ "version": "0.0.0",
5
+ "author": "Matthias Steiner",
6
+ "repository": "github:papack/ssr",
7
+ "homepage": "https://github.com/papack/ssr",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "module": "dist/index.mjs",
12
+ "main": "./dist/index.cjs",
13
+ "types": "./dist/index.d.mts",
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsdown"
21
+ },
22
+ "devDependencies": {
23
+ "@types/bun": "latest",
24
+ "tsdown": "^0.16.8"
25
+ }
26
+ }
package/readme.md ADDED
@@ -0,0 +1,171 @@
1
+ # @papack/ssr
2
+
3
+ Minimal server-side rendering framework with JSX-to-string rendering.
4
+
5
+ ## Core Idea
6
+
7
+ - JSX rendered directly to HTML strings
8
+ - Components can be async
9
+ - No client-side JavaScript required
10
+ - No state, no reactivity, no hydration
11
+ - Every route is a plain function
12
+ - Built on Node.js `http`
13
+ - Optional TTL-based response caching
14
+
15
+ This is **SSR only**. request -> render -> response.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @papack/ssr
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```ts
26
+ import { Router } from "@papack/ssr";
27
+
28
+ const router = new Router({
29
+ siteName: "Example",
30
+ });
31
+
32
+ router.html({ path: "/" }, (ctx) => {
33
+ return <h1>{ctx.siteName}</h1>;
34
+ });
35
+
36
+ router.listen(3000);
37
+ ```
38
+
39
+ ## Routing
40
+
41
+ ### HTML
42
+
43
+ ```ts
44
+ router.html({ path: "/product/:id" }, (ctx) => {
45
+ return <h1>Product {ctx.params.id}</h1>;
46
+ });
47
+ ```
48
+
49
+ - Returns HTML
50
+ - `<!DOCTYPE html>` is added automatically
51
+ - `Content-Type: text/html; charset=utf-8`
52
+
53
+ ### CSS
54
+
55
+ ```ts
56
+ router.css({ path: "/styles/main.css" }, () => {
57
+ return `
58
+ body { font-family: system-ui; }
59
+ `;
60
+ });
61
+ ```
62
+
63
+ - Returns plain strings
64
+ - `Content-Type: text/css; charset=utf-8`
65
+
66
+ ### JavaScript
67
+
68
+ ```ts
69
+ router.js({ path: "/scripts/app.js" }, () => {
70
+ return `console.log("loaded");`;
71
+ });
72
+ ```
73
+
74
+ ## Params
75
+
76
+ ```ts
77
+ router.html({ path: "/blog/:slug" }, (ctx) => {
78
+ return <h1>{ctx.params.slug}</h1>;
79
+ });
80
+ ```
81
+
82
+ ## Context (ctx)
83
+
84
+ ```ts
85
+ const router = new Router({
86
+ db,
87
+ version: "1.0",
88
+ });
89
+ ```
90
+
91
+ ```ts
92
+ (ctx) => {
93
+ ctx.req; // IncomingMessage
94
+ ctx.res; // ServerResponse
95
+ };
96
+ ```
97
+
98
+ ## JSX Rendering
99
+
100
+ ### Async Components
101
+
102
+ ```ts
103
+ async function User({ id }) {
104
+ const user = await db.getUser(id);
105
+ return <p>{user.name}</p>;
106
+ }
107
+ ```
108
+
109
+ ### `<For />`
110
+
111
+ ```tsx
112
+ <For each={items}>{(item) => <li>{item}</li>}</For>
113
+ ```
114
+
115
+ - Single render function
116
+ - No keys
117
+ - No diffing
118
+ - SSR-only
119
+
120
+ ### `<Show />`
121
+
122
+ ```tsx
123
+ <Show when={loggedIn}>
124
+ <Dashboard />
125
+ </Show>
126
+ ```
127
+
128
+ ## Error Handling
129
+
130
+ ### 404
131
+
132
+ ```ts
133
+ router.notFound(() => {
134
+ return <h1>Not Found</h1>;
135
+ });
136
+ ```
137
+
138
+ ### 500
139
+
140
+ ```ts
141
+ router.error((ctx, err) => {
142
+ return (
143
+ <>
144
+ <h1>Error</h1>
145
+ <pre>{String(err)}</pre>
146
+ </>
147
+ );
148
+ });
149
+ ```
150
+
151
+ ## TTL Cache (Optional)
152
+
153
+ ```ts
154
+ router.html({ path: "/", ttl: 5000 }, () => {
155
+ return <h1>Cached</h1>;
156
+ });
157
+ ```
158
+
159
+ - Per-route in-memory cache
160
+ - Cache key includes route params
161
+ - TTL in milliseconds
162
+
163
+ ## Cookies / Headers
164
+
165
+ ```ts
166
+ router.html({ path: "/login" }, (ctx) => {
167
+ ctx.res.setHeader("Set-Cookie", "session=abc; Path=/; HttpOnly");
168
+
169
+ return "ok";
170
+ });
171
+ ```