@papack/ssr 0.0.0 → 0.1.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 +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.mts +0 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/license.md +1 -1
- package/package.json +2 -1
- package/readme.md +9 -14
package/dist/index.cjs
CHANGED
|
@@ -1 +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
|
|
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 r={},a=!0;for(let e=0;e<n.parts.length;e++){let t=n.parts[e],o=i[e];if(!t||!o){a=!1;break}if(t.startsWith(`:`))r[t.slice(1)]=o;else if(t!==o){a=!1;break}}if(!a)continue;let o={req:e,res:t,params:r,...this.ctx},s=await n.handler(o);n.contentType.startsWith(`text/html`)&&(s=`<!DOCTYPE html>`+s),t.statusCode=200,t.setHeader(`content-type`,n.contentType),t.end(s);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)}css(e,t){this.addRoute(e.path,`text/css; charset=utf-8`,t)}js(e,t){this.addRoute(e.path,`application/javascript; charset=utf-8`,t)}notFound(e){this.notFoundHandler=e}error(e){this.errorHandler=e}listen(e,t){this.server.listen(e,t)}addRoute(e,t,n){let r=this.splitPath(e);this.routes.push({parts:r,contentType:t,handler:n})}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;
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.mts
CHANGED
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +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;
|
|
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;KAQC,YAAA;;;cAIQ;UFjBe,MAAO;EAAA,QAAA,MAAA;EAAA,QAAA,GAAA;EAM9B,QAAA,eAAS;EAET,QAAK,YAAA;EAAG,WAAA,CAAA,GAAA,EEoBM,GFpBN;EAAY,QAAA,WAAA;EAAkB,IAAA,CAAA,IAAA,EEgIjC,YFhIiC,EAAA,OAAA,EAAA,CAAA,GAAA,EEiIxB,OFjIwB,CEiIhB,GFjIgB,CAAA,EAAA,GAAA,MAAA,GEiIE,OFjIF,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAAY,GAAA,CAAA,IAAA,EEuI7C,YFvI6C,EAAA,OAAA,EAAA,CAAA,GAAA,EEwIpC,OFxIoC,CEwI5B,GFxI4B,CAAA,EAAA,GAAA,MAAA,GEwIV,OFxIU,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAApB,EAAA,CAAA,IAAA,EE8IzB,YF9IyB,EAAA,OAAA,EAAA,CAAA,GAAA,EE+IhB,OF/IgB,CE+IR,GF/IQ,CAAA,EAAA,GAAA,MAAA,GE+IU,OF/IV,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAAO,QAAA,CAAA,OAAA,EAAA,CAAA,GAAA,EEoJhB,OFpJgB,CEoJR,GFpJQ,CAAA,EAAA,GAAA,MAAA,GEoJU,OFpJV,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAE9B,KAAA,CAAA,OAAK,EAAA,CAAA,GAAA,EEuJE,OFrJD,CEqJS,GFrJT,CAAA,EAAA,GAAA,EAAA,OAAA,EAAA,GAAA,MAAA,GEqJyC,OFrJzC,CAAA,MAAA,CAAA,CAAA,EAAA,IAAA;EAGI,MAAG,CAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,EAAA,GAAA,GAAA,IAAA,CAAA,EAAA,IAAA;EACiB,QAAA,QAAA;EACjC,QAAA,SAAA;;;;KGnBJ,SAAA;;;;AJKW,iBIAM,IAAA,CJAQ,CAAA,EIAA,SJAQ,CAAA,EIAI,OJAJ,CAAA,MAAA,CAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +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
|
|
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 r={},a=!0;for(let e=0;e<n.parts.length;e++){let t=n.parts[e],o=i[e];if(!t||!o){a=!1;break}if(t.startsWith(`:`))r[t.slice(1)]=o;else if(t!==o){a=!1;break}}if(!a)continue;let o={req:e,res:t,params:r,...this.ctx},s=await n.handler(o);n.contentType.startsWith(`text/html`)&&(s=`<!DOCTYPE html>`+s),t.statusCode=200,t.setHeader(`content-type`,n.contentType),t.end(s);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)}css(e,t){this.addRoute(e.path,`text/css; charset=utf-8`,t)}js(e,t){this.addRoute(e.path,`application/javascript; charset=utf-8`,t)}notFound(e){this.notFoundHandler=e}error(e){this.errorHandler=e}listen(e,t){this.server.listen(e,t)}addRoute(e,t,n){let r=this.splitPath(e);this.routes.push({parts:r,contentType:t,handler:n})}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
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
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 Route<Ctx> = {\n parts: string[];\n contentType: string;\n handler: (ctx: Context<Ctx>) => string | Promise<string>;\n};\n\ntype RouteOptions = {\n path: string;\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 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 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);\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);\n }\n\n js(\n opts: RouteOptions,\n handler: (ctx: Context<Ctx>) => string | Promise<string>,\n ) {\n this.addRoute(opts.path, \"application/javascript; charset=utf-8\", handler);\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 ) {\n const parts = this.splitPath(path);\n\n this.routes.push({\n parts,\n contentType,\n handler,\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
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papack/ssr",
|
|
3
3
|
"description": "Minimal server-side rendering framework",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"author": "Matthias Steiner",
|
|
6
6
|
"repository": "github:papack/ssr",
|
|
7
7
|
"homepage": "https://github.com/papack/ssr",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"build": "tsdown"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
+
"typescript": "^5.9.3",
|
|
23
24
|
"@types/bun": "latest",
|
|
24
25
|
"tsdown": "^0.16.8"
|
|
25
26
|
}
|
package/readme.md
CHANGED
|
@@ -10,9 +10,8 @@ Minimal server-side rendering framework with JSX-to-string rendering.
|
|
|
10
10
|
- No state, no reactivity, no hydration
|
|
11
11
|
- Every route is a plain function
|
|
12
12
|
- Built on Node.js `http`
|
|
13
|
-
- Optional TTL-based response caching
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
> **SSR only**. request -> render -> response.
|
|
16
15
|
|
|
17
16
|
## Install
|
|
18
17
|
|
|
@@ -148,18 +147,6 @@ router.error((ctx, err) => {
|
|
|
148
147
|
});
|
|
149
148
|
```
|
|
150
149
|
|
|
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
150
|
## Cookies / Headers
|
|
164
151
|
|
|
165
152
|
```ts
|
|
@@ -169,3 +156,11 @@ router.html({ path: "/login" }, (ctx) => {
|
|
|
169
156
|
return "ok";
|
|
170
157
|
});
|
|
171
158
|
```
|
|
159
|
+
|
|
160
|
+
## Sessions
|
|
161
|
+
|
|
162
|
+
**@papack/ssr** works well with **@papack/session**. Rendering is strictly request-based, while sessions are handled explicitly via cookies at the `node:http` level. Session data is available through `ctx.req` and `ctx.res`, fitting naturally into the request -> render -> response flow without implicit state.
|
|
163
|
+
|
|
164
|
+
## Cache
|
|
165
|
+
|
|
166
|
+
**@papack/ssr** intentionally has no built-in caching. If caching is required, **@papack/cache** can be added explicitly on top to store rendered output or data, keeping cache behavior opt-in and the core SSR model predictable.
|