@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 +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.mts +66 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/license.md +21 -0
- package/package.json +26 -0
- package/readme.md +171 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
```
|