@lengkapp/edge 0.0.5 → 0.0.7

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.
@@ -0,0 +1 @@
1
+ !function(){if(!window.__edge){window.__edge=!0;var e=document,n=e.head,t=e.documentElement,r=["get","post","put","patch","delete"].map(function(e){return"_"+e}),i={post:1,put:1,patch:1},o=r.map(function(e){return"["+e+"]"}).join(","),a=new Set(["click","dblclick","mousedown","mouseup","mouseover","mouseout","mousemove","contextmenu","focusin","focusout","keydown","keyup","keypress","change","input","submit","reset","wheel","touchstart","touchend","touchmove"]),u=new WeakSet,c=new WeakSet,s=new WeakMap,f=new WeakMap,d=new Map,l=new Set,h=new Set,v=e.createElement("style");v.textContent=".aj-skel-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;max-width:100%;max-height:600px;display:flex;align-items:center;justify-content:center}.aj-skel-wrap svg{width:1.75em;height:1.75em;color:#c7c7c7;animation:aj-pulse 1.4s ease-in-out infinite}@keyframes aj-pulse{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:1;transform:scale(1)}}.aj-retry-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;max-height:600px;display:flex;align-items:center;justify-content:center}.aj-retry{width:2.5em;height:2.5em;padding:0;cursor:pointer;background:#007bff;color:#fff;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease}.aj-retry:hover{background:#0056b3}.aj-retry:hover svg{transform:rotate(-90deg)}.aj-retry svg{width:1.25em;height:1.25em;transition:transform .3s ease}",n.appendChild(v);var p=!1,w=new Set,m=new Set,g=!1;new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){1===e.nodeType&&w.add(e)}),e.removedNodes.forEach(function(e){1===e.nodeType&&m.add(e)})}),g||(g=!0,queueMicrotask(_))}).observe(t,{childList:!0,subtree:!0}),"loading"===e.readyState?e.addEventListener("DOMContentLoaded",C,{once:!0}):C()}function k(e){for(var n=null,t=0;t<r.length;t++)if(e.hasAttribute(r[t])){n=r[t];break}if(!n)return null;var o=n.slice(1);return{url:e.getAttribute(n),method:o.toUpperCase(),hasBody:!!i[o],target:e.getAttribute("_target"),trigger:e.getAttribute("_trigger"),form:e.getAttribute("_form"),json:e.getAttribute("_json"),skeleton:"false"!==e.getAttribute("_skeleton"),retry:"false"!==e.getAttribute("_retry")}}function y(e){return(e.trigger||("this"===e.target?"load":"click")).split(",").map(function(e){return e.trim().toLowerCase()})}function b(e){return e?e.replace(/^\[|\]$/g,"").split(",").map(function(e){return e.trim().replace(/^['"]|['"]$/g,"")}).filter(Boolean):[]}function x(t,r){if(!c.has(t)){c.add(t);var i=function(n,t){if(!t.target||"this"===t.target)return n;var r=s.get(n);if(r&&r.isConnected)return r;var i=e.querySelector(t.target);return i&&s.set(n,i),i}(t,r);if(i){r.skeleton&&function(e){e.innerHTML='<div class="aj-skel-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg></div>'}(i);var o={method:r.method};if(r.hasBody)if(r.json){var a={};r.json.split(",").forEach(function(n){n=n.trim();var t=e.querySelector('[name="'+n+'"]');a[n]=t?t.value:void 0}),o.headers={"Content-Type":"application/json"},o.body=JSON.stringify(a)}else if(r.form){var u=e.getElementById(r.form);o.headers={"Content-Type":"application/x-www-form-urlencoded"},o.body=u?new URLSearchParams(new FormData(u)).toString():""}fetch(r.url,o).then(function(t){if(!t.ok)throw Error("HTTP "+t.status);var r;return b((r=t.headers).get("x-css-required")).forEach(function(t){if(!l.has(t)){l.add(t);var r=e.createElement("link");r.rel="stylesheet",r.href=t,n.appendChild(r)}}),b(r.get("x-js-required")).forEach(function(t){if(!l.has(t)){l.add(t);var r=e.createElement("script");r.src=t,n.appendChild(r)}}),t.text()}).then(function(n){!function(n,t){var r,i,o;n.innerHTML=t,r=n,i=Array.prototype.slice.call(r.querySelectorAll("script")),o=0,function n(){if(!(o>=i.length)){for(var t=i[o],r=e.createElement("script"),a=0;a<t.attributes.length;a++)r.setAttribute(t.attributes[a].name,t.attributes[a].value);if(t.nonce&&(r.nonce=t.nonce),t.src){if(h.has(t.src))return t.remove(),o++,void n();h.add(t.src),r.onload=r.onerror=function(){o++,n()},t.replaceWith(r)}else r.textContent="{\n"+t.textContent+"\n}",t.replaceWith(r),o++,n()}}(),M(n)}(i,n)}).catch(function(n){console.error(n),r.retry&&function(n,t){n.innerHTML="";var r=e.createElement("div");r.className="aj-retry-wrap";var i=e.createElement("button");i.type="button",i.className="aj-retry",i.setAttribute("aria-label","Retry"),i.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',i.addEventListener("click",t,{once:!0}),r.appendChild(i),n.appendChild(r)}(i,function(){x(t,r)})}).finally(function(){c.delete(t)})}else c.delete(t)}}function j(e){if(!u.has(e)){u.add(e);var n=k(e);n&&y(n).forEach(function(t){if("load"===t||"domcontentloaded"===t||"ready"===t)queueMicrotask(function(){x(e,n)});else if("visible"===t||"intersect"===t){f.set(e,!1);var r=new IntersectionObserver(function(t){t.forEach(function(t){if(!t.target.isConnected)return r.disconnect(),void d.delete(e);var i=t.isIntersecting&&function(e){var n=getComputedStyle(e);if("none"===n.display||"hidden"===n.visibility||"0"===n.opacity)return!1;var t=e.getBoundingClientRect();return t.width>0&&t.height>0}(e);i&&!f.get(e)&&x(e,n),f.set(e,i)})});r.observe(e),d.set(e,r)}else a.has(t)||e.addEventListener(t,function(){x(e,n)})})}}function M(e){1===e.nodeType&&(e.matches&&e.matches(o)&&j(e),e.querySelectorAll&&e.querySelectorAll(o).forEach(j))}function S(e){d.size&&d.forEach(function(n,t){(e===t||e.contains(t))&&(n.disconnect(),d.delete(t),u.delete(t))})}function _(){g=!1;var e=Array.from(w);w.clear();var n=Array.from(m);m.clear(),e.forEach(M),n.forEach(S)}function C(){p||(p=!0,a.forEach(function(n){var t="click"===n||"submit"===n;e.addEventListener(n,function(e){var r=e.target;if(r instanceof Element){var i=r.closest(o);if(i){var a=k(i);a&&-1!==y(a).indexOf(n)&&(t&&e.preventDefault(),x(i,a))}}},!t&&{passive:!0})})),M(t)}}();
@@ -1,9 +1,6 @@
1
- // Type definitions for edge-server
2
-
3
- /// <reference lib="dom" />
4
1
  /// <reference types="@cloudflare/workers-types" />
5
2
 
6
- export class Context {
3
+ declare class Context {
7
4
  constructor(
8
5
  request: Request,
9
6
  env: any,
@@ -20,7 +17,6 @@ export class Context {
20
17
  headers: Headers;
21
18
  url: URL;
22
19
 
23
- // Private members (not accessible, but present)
24
20
  private _rawCookie: string;
25
21
  private _cookies: Record<string, string> | null;
26
22
 
@@ -70,8 +66,6 @@ export function jsx(type: any, props: Record<string, any> | null, ...children: a
70
66
 
71
67
  export function renderToString(node: any): string;
72
68
 
73
- // Route option interfaces
74
-
75
69
  interface AuthOptions {
76
70
  role?: string;
77
71
  scopes?: string[];
@@ -86,7 +80,6 @@ interface CorsOptions {
86
80
  origin?: string;
87
81
  methods?: string;
88
82
  headers?: string;
89
- credentials?: boolean;
90
83
  }
91
84
 
92
85
  interface CacheOptions {
@@ -104,12 +97,15 @@ interface RouteOptions {
104
97
  log?: boolean;
105
98
  }
106
99
 
107
- type RouteHandler = (ctx: Context) => Response | JSXElement | any | Promise<Response | JSXElement | any>;
100
+ type RouteHandler = (ctx: Context) => Response | JSXElement | void | Promise<Response | JSXElement | void>;
108
101
 
109
102
  export class Edge {
110
103
  constructor();
111
104
 
112
- // Route registration methods
105
+ authKvBinding: string;
106
+ rateLimitKvBinding: string;
107
+ scheduledHandler: ((controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>) | null;
108
+
113
109
  get(path: string, handler: RouteHandler): void;
114
110
  get(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
115
111
 
@@ -133,11 +129,9 @@ export class Edge {
133
129
 
134
130
  scheduled(handler: (controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>): void;
135
131
 
136
- // Main fetch handler
137
132
  fetch(request: Request, env: any, executionCtx: ExecutionContext): Promise<Response>;
138
133
  }
139
134
 
140
- // JSX namespace support
141
135
  declare global {
142
136
  namespace JSX {
143
137
  interface Element extends JSXElement {}
@@ -0,0 +1 @@
1
+ var t=Object.defineProperty,e=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.prototype.hasOwnProperty,r=(e,n)=>{for(var o in n)t(e,o,{get:n[o],enumerable:!0})},s=(r,s,i,c)=>{if(s&&"object"==typeof s||"function"==typeof s)for(let a of n(s))o.call(r,a)||a===i||t(r,a,{get:()=>s[a],enumerable:!(c=e(s,a))||c.enumerable});return r},i=e=>s(t({},"__esModule",{value:!0}),e),c={};function a(t){const e=[],n=t.length;let o=1;for(let r=1;r<=n;r++)r!==n&&47!==t.charCodeAt(r)||(r>o&&e.push(t.slice(o,r)),o=r+1);return e}function l(t){const e=t.length;return e>1&&47===t.charCodeAt(e-1)?t.slice(0,-1):t}r(c,{Edge:()=>$,Fragment:()=>f,jsx:()=>h,renderToString:()=>A}),module.exports=i(c);var u=class{constructor(t,e,n,o={},r=null){this.req=t,this.env=e,this.executionCtx=n,this.params=o,this.status=200,this.headers=new Headers,this.t=t.headers.get("Cookie")||"",this.o=null,this.url=r||new URL(t.url)}i(){if(null===this.o){const t=Object.create(null);if(this.t)for(const e of this.t.split(";")){const n=e.trim();if(!n)continue;const o=n.indexOf("=");if(o>0){const e=decodeURIComponent(n.slice(0,o)),r=decodeURIComponent(n.slice(o+1));t[e]=r}}this.o=t}return this.o}getCookie(t){return this.i()[t]??null}get query(){return this.url.searchParams}setCookie(t,e,n={}){let o=`${encodeURIComponent(t)}=${encodeURIComponent(e)}`;n.path&&(o+="; Path="+n.path),n.domain&&(o+="; Domain="+n.domain),void 0!==n.maxAge&&(o+="; Max-Age="+n.maxAge),n.expires&&(o+="; Expires="+n.expires.toUTCString()),n.secure&&(o+="; Secure"),n.httpOnly&&(o+="; HttpOnly"),n.sameSite&&(o+="; SameSite="+n.sameSite),this.headers.append("Set-Cookie",o)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}text(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","text/plain"),new Response(t,{status:e,headers:o})}json(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:o})}html(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","text/html"),new Response(t,{status:e,headers:o})}l(t){if(0===Object.keys(t).length)return this.headers;const e=new Headers(this.headers);for(const n in t)Object.prototype.hasOwnProperty.call(t,n)&&e.set(n,t[n]);return e}},f=Symbol("Fragment");function h(t,e,...n){return{type:t,props:e||{},children:n.flat(1/0),u:!0}}var d=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),p=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]),m=new Set(["animation-iteration-count","border-image-outset","border-image-slice","border-image-width","box-flex","box-flex-group","box-ordinal-group","column-count","columns","flex","flex-grow","flex-positive","flex-shrink","flex-negative","flex-order","grid-row","grid-row-end","grid-row-span","grid-row-start","grid-column","grid-column-end","grid-column-span","grid-column-start","font-weight","line-clamp","line-height","opacity","order","orphans","tab-size","widows","z-index","zoom","fill-opacity","flood-opacity","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-miterlimit","stroke-opacity","stroke-width"]),w={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},y=/[&<>"']/g;function g(t){return y.lastIndex=0,y.test(t)?t.replace(y,t=>w[t]):t}var T=new Map,b=2e3;function C(t){const e=T.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return T.size>=b&&T.clear(),T.set(t,n),n}function E(t){if(!t||"object"!=typeof t)return"";const e=Object.entries(t);return 0===e.length?"":e.map(([t,e])=>{let n=C(t);return"number"!=typeof e||m.has(n)||(e+="px"),`${n}:${e}`}).join(";")}function A(t){if(null==t||"boolean"==typeof t)return"";if("string"==typeof t||"number"==typeof t)return g(t+"");if(Array.isArray(t)){const e=[];for(let n=0;n<t.length;n++)e.push(A(t[n]));return e.join("")}if(!t.u)return g(t+"");const{type:e,props:n,children:o}=t;if(e===f){const t=[];for(let e=0;e<o.length;e++)t.push(A(o[e]));return t.join("")}if("symbol"==typeof e)return"";if("function"==typeof e)return A(e({...n,children:o}));const r=d.has(e),s=[];for(const t in n){if(!Object.prototype.hasOwnProperty.call(n,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=n[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let o=t;if("className"===t||"class"===t?o="class":"htmlFor"!==t&&"for"!==t||(o="for"),p.has(o))!0===e&&s.push(" "+o);else if(!0!==e){if("style"===o&&"object"==typeof e){const t=E(e);t&&s.push(` style="${g(t)}"`);continue}s.push(` ${o}="${g(e+"")}"`)}else s.push(` ${o}="true"`)}const i=s.join("");let c="";if(n&&n.dangerouslySetInnerHTML&&null!=n.dangerouslySetInnerHTML.h)c=n.dangerouslySetInnerHTML.h;else{const t=[];for(let e=0;e<o.length;e++)t.push(A(o[e]));c=t.join("")}return r?`<${e}${i}/>`:`<${e}${i}>${c}</${e}>`}var O=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},x=class{constructor(){this.root=new O}add(t,e,n){let o=this.root;const r=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0))r.push(n.slice(1)),o.paramChild||(o.paramChild=new O),o=o.paramChild;else{o.children||(o.children=new Map);let t=o.children.get(n);t||(t=new O,o.children.set(n,t)),o=t}}o.handler={handler:e,options:n,paramNames:r}}match(t){const e=[],n=this.p(this.root,t,0,e);if(!n)return null;const{handler:o,options:r,paramNames:s}=n,i={};for(let t=0;t<s.length;t++)i[s[t]]=e[t];return{handler:o,options:r,params:i}}p(t,e,n,o){if(n===e.length)return t.handler;const r=e[n];if(t.children){const s=t.children.get(r);if(s){const t=this.p(s,e,n+1,o);if(t)return t}}if(t.paramChild){o.push(r);const s=this.p(t.paramChild,e,n+1,o);if(s)return s;o.pop()}return null}},S=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],$=class{constructor(){this.staticRoutes=new Map;for(const t of S)this.staticRoutes.set(t,new Map);this.dynamicTries={};for(const t of S)this.dynamicTries[t]=new x;this.authKvBinding="AUTH_KV",this.rateLimitKvBinding="RATE_LIMIT_KV",this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}},this.scheduledHandler=null}m(t,e,n,o){if("boolean"==typeof n&&(n={auth:n}),"function"==typeof n&&(o=n,n={}),"function"!=typeof o)throw Error(`Handler for ${t} ${e} must be a function`);const r={...this.defaults,...n||{}};if(-1===e.indexOf(":")){const n=l(e);this.staticRoutes.get(t).set(n,{handler:o,options:r})}else{const n=a(e);this.dynamicTries[t].add(n,o,r)}}get(t,e,n){this.m("GET",t,e,n)}post(t,e,n){this.m("POST",t,e,n)}put(t,e,n){this.m("PUT",t,e,n)}delete(t,e,n){this.m("DELETE",t,e,n)}patch(t,e,n){this.m("PATCH",t,e,n)}options(t,e,n){this.m("OPTIONS",t,e,n)}head(t,e,n){this.m("HEAD",t,e,n)}scheduled(t){this.scheduledHandler=t}async T(t,e){if(!e)return!0;const n=t.getCookie("auth_token")||t.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!n)return!1;const o=t.env[this.authKvBinding];if(!o)return!1;const r=await o.get(n);if(!r)return!1;if("object"==typeof e)try{const t=JSON.parse(r);if(e.role&&t.role!==e.role)return!1;if(e.scopes){const n=t.scopes||[];if(!e.scopes.every(t=>n.includes(t)))return!1}}catch{}return!0}async C(t,e){if(!e)return!0;const n=!0===e?{}:e,{max:o=100,window:r=60}=n,s="rl:"+(t.req.headers.get("CF-Connecting-IP")||"unknown"),i=t.env[this.rateLimitKvBinding];if(!i)return!0;let c=await i.get(s,"json")||0;return!(c>=o||(c++,await i.put(s,JSON.stringify(c),{expirationTtl:r}),0))}A(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors;t.headers.set("Access-Control-Allow-Origin",n.origin||"*"),t.headers.set("Access-Control-Allow-Methods",n.methods||"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"),t.headers.set("Access-Control-Allow-Headers",n.headers||"Content-Type, Authorization"),t.headers.set("Access-Control-Max-Age","86400")}O(t,e){e&&console.log(`${t.req.method} ${t.req.url} - ${t.status}`)}S(t,e){const n=t.headers.get("Accept-Encoding")||"";let o=null;if(n.includes("gzip")?o="gzip":n.includes("deflate")&&(o="deflate"),!o||!e.body)return e;const r=e.body.pipeThrough(new CompressionStream(o)),s=new Headers(e.headers);return s.set("Content-Encoding",o),s.set("Vary","Accept-Encoding"),s.delete("Content-Length"),new Response(r,{status:e.status,statusText:e.statusText,headers:s})}async $(t,e){if(!e)return!0;if("function"==typeof e)try{return!!await e(t)}catch{return!1}return!0}async _(t){const e=caches.default;return await e.match(t)||null}async k(t,e,n){if(!n)return;const o=!0===n?{}:n,{ttl:r=3600,staleWhileRevalidate:s=0}=o,i=caches.default,c=e.clone();c.headers.set("Cache-Control",`max-age=${r}${s>0?", stale-while-revalidate="+s:""}`),c.headers.delete("Set-Cookie"),await i.put(t,c)}async fetch(t,e,n){const o=new URL(t.url),r=l(o.pathname),s=t.method,i=this.staticRoutes.get(s);if(i){const s=i.get(r);if(s)return this.P(s.handler,s.options,t,e,n,{},o)}const c=this.dynamicTries[s];if(c){const s=a(r),i=c.match(s);if(i)return this.P(i.handler,i.options,t,e,n,i.params,o)}return new Response("Not Found",{status:404})}async P(t,e,n,o,r,s,i){const c=new u(n,o,r,s,i);if(!await this.$(c,e.validate))return c.text("Validation failed",400);if(!await this.T(c,e.auth))return c.text("Unauthorized",401);if(!await this.C(c,e.rateLimit))return c.text("Too Many Requests",429);let a,l=null;if(e.cache&&"GET"===n.method&&(l=await this._(n),l))return this.A(c,e.cors),this.O(c,e.log),l;this.A(c,e.cors);try{const e=await t(c);a=e&&e.u?c.html(A(e)):e instanceof Response?e:c.text("OK"),c.status=a.status}catch(t){console.error(t),a=c.text("Internal Server Error",500),c.status=500}return e.cache&&"GET"===n.method&&200===a.status&&c.executionCtx.waitUntil(this.k(n,a.clone(),e.cache)),e.compress&&(a=this.S(n,a)),this.O(c,e.log),a}};
package/package.json CHANGED
@@ -1,38 +1,45 @@
1
1
  {
2
2
  "name": "@lengkapp/edge",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Edge framework used by Lengkapp",
5
- "main": "edge-server.js",
6
- "types": "./edge-server.d.ts",
5
+ "main": "dist/edge-server.js",
6
+ "types": "dist/edge-server.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
- "types": "./edge-server.d.ts",
10
- "node": "./edge-server.js",
11
- "default": "./edge-server.js"
9
+ "types": "./dist/edge-server.d.ts",
10
+ "node": "./dist/edge-server.js",
11
+ "default": "./dist/edge-server.js"
12
12
  },
13
13
  "./client": {
14
- "types": "./edge-client.d.ts",
15
- "default": "./edge-client.js"
14
+ "types": "./dist/edge-client.d.ts",
15
+ "default": "./dist/edge-client.js"
16
16
  },
17
17
  "./server": {
18
- "types": "./edge-server.d.ts",
19
- "default": "./edge-server.js"
18
+ "types": "./dist/edge-server.d.ts",
19
+ "default": "./dist/edge-server.js"
20
20
  },
21
21
  "./package.json": "./package.json"
22
22
  },
23
23
  "files": [
24
- "edge-client.js",
25
- "edge-client.d.ts",
26
- "edge-server.js",
27
- "edge-server.d.ts"
24
+ "dist"
28
25
  ],
29
26
  "scripts": {
30
- "test": "echo \"No tests yet\""
27
+ "build": "node build.mjs",
28
+ "pre": "npm run build",
29
+ "publish": "npm run pre && npm publish"
31
30
  },
32
31
  "publishConfig": {
33
32
  "access": "public"
34
33
  },
35
- "keywords": ["cloudflare workers", "minimal server library", "minimal client library"],
34
+ "keywords": [
35
+ "cloudflare workers",
36
+ "minimal server library",
37
+ "minimal client library"
38
+ ],
36
39
  "author": "Yasir Haris",
37
- "license": "MIT"
38
- }
40
+ "license": "MIT",
41
+ "devDependencies": {
42
+ "esbuild": "^0.28.2",
43
+ "terser": "^5.51.2"
44
+ }
45
+ }
package/edge-client.js DELETED
@@ -1,287 +0,0 @@
1
- (() => {
2
- 'use strict';
3
-
4
- const d = document,
5
- body = d.body,
6
- head = d.head;
7
-
8
- // ---------- Constants and caches ----------
9
- const VALID_EVENTS = new Set([
10
- 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
11
- 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu',
12
- 'focus', 'blur', 'focusin', 'focusout',
13
- 'keydown', 'keyup', 'keypress',
14
- 'change', 'input', 'submit', 'reset',
15
- 'load', 'DOMContentLoaded', 'ready',
16
- 'scroll', 'resize', 'wheel', 'touchstart', 'touchend', 'touchmove',
17
- 'visible', 'intersect'
18
- ]);
19
-
20
- // WeakMaps for per‑element caches
21
- const triggerCache = new WeakMap(); // element -> Set of event names (lowercase)
22
- const targetCache = new WeakMap(); // element -> target DOM node (resolved)
23
- const resourceCache = new Set(); // already injected CSS/JS URLs
24
-
25
- // ---------- Injected base styles ----------
26
- const injectBaseStyles = () => {
27
- if (d.getElementById('df-style')) return;
28
- const style = d.createElement('style');
29
- style.id = 'df-style';
30
- style.textContent = `
31
- .df-skeleton {
32
- position: absolute;
33
- inset: 0;
34
- background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
35
- background-size: 200% 100%;
36
- animation: df-shimmer 1.5s ease-in-out infinite;
37
- border-radius: inherit;
38
- pointer-events: none;
39
- box-sizing: border-box;
40
- }
41
- @keyframes df-shimmer {
42
- 0% {
43
- background-position: -200% 0;
44
- }
45
- 100% {
46
- background-position: 200% 0;
47
- }
48
- }
49
-
50
- .df-retry {
51
- display:inline-block; padding:8px 16px; background:#007bff; color:#fff;
52
- border-radius:4px; cursor:pointer; text-decoration:none; font-size:14px;
53
- }
54
- .df-retry:hover { background:#0056b3; }
55
- `;
56
- head.appendChild(style);
57
- };
58
- injectBaseStyles();
59
-
60
- // ---------- Pre‑built templates (cloned on use) ----------
61
- const skeletonTemplate = d.createElement('template');
62
- skeletonTemplate.innerHTML = `
63
- <div class="df-skeleton"></div>`;
64
- const retryTemplate = d.createElement('template');
65
- retryTemplate.innerHTML = `<span class="df-retry">Retry</span>`;
66
-
67
- // ---------- Resource helpers ----------
68
- const parseHeaderList = (str) => {
69
- if (!str) return [];
70
- return str.replace(/^\[|\]$/g, '').split(',')
71
- .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
72
- .filter(Boolean);
73
- };
74
-
75
- const injectResources = (resources, type) => {
76
- resources.forEach(url => {
77
- if (resourceCache.has(url)) return;
78
- resourceCache.add(url);
79
- if (type === 'css') {
80
- const link = d.createElement('link');
81
- link.rel = 'stylesheet'; link.href = url;
82
- head.appendChild(link);
83
- } else if (type === 'js') {
84
- const script = d.createElement('script');
85
- script.src = url;
86
- body.appendChild(script);
87
- }
88
- });
89
- };
90
-
91
- // ---------- Visibility helpers ----------
92
- const isElementActuallyVisible = (el) => {
93
- const style = getComputedStyle(el);
94
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
95
- const rect = el.getBoundingClientRect();
96
- return rect.width > 0 && rect.height > 0;
97
- };
98
-
99
- // Shared IntersectionObserver
100
- let intersectionObserver = null;
101
- const getIO = () => {
102
- if (!intersectionObserver) {
103
- intersectionObserver = new IntersectionObserver((entries) => {
104
- entries.forEach(entry => {
105
- const el = entry.target;
106
- const visible = entry.isIntersecting && isElementActuallyVisible(el);
107
- if (visible && el.dataset.wasVisible !== 'true') {
108
- run(el);
109
- }
110
- el.dataset.wasVisible = visible ? 'true' : 'false';
111
- });
112
- }, { threshold: 0 });
113
- }
114
- return intersectionObserver;
115
- };
116
-
117
- // ---------- Core run function ----------
118
- const run = async (el) => {
119
- if (el.dataset.running === 'true') return;
120
- el.dataset.running = 'true';
121
-
122
- const post = el.hasAttribute('_post');
123
- const url = el.getAttribute('_post') || el.getAttribute('_get');
124
- let bodyData, headers = {};
125
-
126
- if (post) {
127
- const formId = el.getAttribute('_form');
128
- const json = el.getAttribute('_json');
129
- if (formId) {
130
- const form = d.getElementById(formId);
131
- if (form) bodyData = new URLSearchParams(new FormData(form));
132
- } else if (json) {
133
- bodyData = JSON.stringify(Object.fromEntries(
134
- json.split(',').map(name => {
135
- const input = d.querySelector(`[name="${name}"]`);
136
- return [name, input ? input.value : ''];
137
- })
138
- ));
139
- headers['Content-Type'] = 'application/json';
140
- }
141
- }
142
-
143
- // Resolve target (cached)
144
- const targetSelector = el.getAttribute('_target');
145
- let container = targetCache.get(el);
146
- if (!container) {
147
- container = targetSelector === 'this' ? el : d.querySelector(targetSelector);
148
- if (container) targetCache.set(el, container);
149
- }
150
- if (!container) {
151
- console.warn('Target not found:', targetSelector);
152
- el.dataset.running = 'false';
153
- return;
154
- }
155
-
156
- container.replaceChildren(skeletonTemplate.content.cloneNode(true));
157
-
158
- try {
159
- const res = await fetch(url, { method: post ? 'POST' : 'GET', body: bodyData, headers });
160
-
161
- // Inject resources from headers
162
- const css = res.headers.get('x-css-required') || res.headers.get('x-css-requiered');
163
- const js = res.headers.get('x-js-required');
164
- if (css) injectResources(parseHeaderList(css), 'css');
165
- if (js) injectResources(parseHeaderList(js), 'js');
166
-
167
- const text = await res.text();
168
- container.innerHTML = text; // MutationObserver picks up new declarative elements
169
- } catch (err) {
170
- console.error(err);
171
- // Show retry (clone template)
172
- const retry = retryTemplate.content.firstElementChild.cloneNode(true);
173
- retry.addEventListener('click', (e) => {
174
- e.preventDefault();
175
- e.stopPropagation();
176
- run(el);
177
- });
178
- container.replaceChildren(retry);
179
- } finally {
180
- el.dataset.running = 'false';
181
- }
182
- };
183
-
184
- // ---------- Trigger parsing and caching ----------
185
- const getTriggerEvents = (el) => {
186
- let events = triggerCache.get(el);
187
- if (events) return events;
188
- events = new Set();
189
- const attr = el.getAttribute('_trigger');
190
- if (attr && attr.trim() !== '') {
191
- attr.split(',').forEach(s => {
192
- const evt = s.trim().toLowerCase();
193
- if (VALID_EVENTS.has(evt)) events.add(evt);
194
- });
195
- } else {
196
- // Default: click (plus load if _target="this")
197
- events.add('click');
198
- if (el.getAttribute('_target') === 'this') events.add('load');
199
- }
200
- triggerCache.set(el, events);
201
- return events;
202
- };
203
-
204
- // ---------- Element initialisation (no per‑element listeners) ----------
205
- const initElement = (el) => {
206
- if (el.dataset.initialized === 'true') return;
207
- el.dataset.initialized = 'true';
208
-
209
- const events = getTriggerEvents(el);
210
-
211
- // Handle load / ready immediately
212
- if (events.has('load') || events.has('domcontentloaded') || events.has('ready')) {
213
- run(el);
214
- }
215
- // Set up visibility observation
216
- if (events.has('visible') || events.has('intersect')) {
217
- el.dataset.wasVisible = 'false';
218
- getIO().observe(el);
219
- }
220
- // All other events are handled by the global delegated listener
221
- };
222
-
223
- // ---------- Global event delegation ----------
224
- const delegatedEvents = new Set([
225
- 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
226
- 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu',
227
- 'focus', 'blur', 'focusin', 'focusout',
228
- 'keydown', 'keyup', 'keypress',
229
- 'change', 'input', 'submit', 'reset',
230
- 'scroll', 'resize', 'wheel', 'touchstart', 'touchend', 'touchmove'
231
- ]);
232
-
233
- // Attach one listener per event type, passive for scroll‑like events
234
- delegatedEvents.forEach(evt => {
235
- const isPassive = ['scroll', 'touchstart', 'touchmove', 'touchend', 'wheel'].includes(evt);
236
- d.addEventListener(evt, (e) => {
237
- const target = e.target;
238
- if (!(target instanceof Element)) return;
239
- const el = target.closest('[_get], [_post]');
240
- if (!el) return;
241
- const events = getTriggerEvents(el);
242
- if (events.has(evt)) {
243
- if (evt === 'click') e.preventDefault();
244
- run(el);
245
- }
246
- }, isPassive ? { passive: true } : false);
247
- });
248
-
249
- // ---------- MutationObserver (batch processing) ----------
250
- let mutationQueue = [];
251
- let mutationScheduled = false;
252
-
253
- const processMutations = () => {
254
- mutationScheduled = false;
255
- const nodes = mutationQueue;
256
- mutationQueue = [];
257
- nodes.forEach(node => {
258
- if (node.nodeType !== 1) return;
259
- if (node.matches('[_get], [_post]')) initElement(node);
260
- node.querySelectorAll('[_get], [_post]').forEach(initElement);
261
- });
262
- };
263
-
264
- const mo = new MutationObserver((mutations) => {
265
- mutations.forEach(m => {
266
- m.addedNodes.forEach(node => {
267
- if (node.nodeType === 1) mutationQueue.push(node);
268
- });
269
- });
270
- if (!mutationScheduled) {
271
- mutationScheduled = true;
272
- Promise.resolve().then(processMutations);
273
- }
274
- });
275
- mo.observe(body, { childList: true, subtree: true });
276
-
277
- // ---------- Initialisation on DOM ready ----------
278
- const initAll = () => {
279
- d.querySelectorAll('[_get], [_post]').forEach(initElement);
280
- };
281
-
282
- if (d.readyState === 'loading') {
283
- d.addEventListener('DOMContentLoaded', initAll, { once: true });
284
- } else {
285
- initAll();
286
- }
287
- })();
package/edge-server.js DELETED
@@ -1,573 +0,0 @@
1
- class Context {
2
- constructor(request, env, executionCtx, params = {}, parsedUrl = null) {
3
- this.req = request;
4
- this.env = env;
5
- this.executionCtx = executionCtx;
6
- this.params = params;
7
- this.status = 200;
8
- this.headers = new Headers();
9
- this._rawCookie = request.headers.get('Cookie') || '';
10
- this._cookies = null; // lazy parsed
11
- this.url = parsedUrl || new URL(request.url); // reuse pre-parsed URL if provided
12
- }
13
-
14
- // Lazy cookie parsing
15
- _ensureCookies() {
16
- if (this._cookies === null) {
17
- const cookies = {};
18
- if (this._rawCookie) {
19
- for (const pair of this._rawCookie.split(';')) {
20
- const trimmed = pair.trim();
21
- if (!trimmed) continue;
22
- const idx = trimmed.indexOf('=');
23
- if (idx > 0) {
24
- const name = decodeURIComponent(trimmed.slice(0, idx));
25
- const value = decodeURIComponent(trimmed.slice(idx + 1));
26
- cookies[name] = value;
27
- }
28
- }
29
- }
30
- this._cookies = cookies;
31
- }
32
- return this._cookies;
33
- }
34
-
35
- getCookie(name) {
36
- return this._ensureCookies()[name] ?? null;
37
- }
38
-
39
- // Lazy query parsing
40
- get query() {
41
- return this.url.searchParams;
42
- }
43
-
44
- setCookie(name, value, options = {}) {
45
- let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
46
- if (options.path) cookie += `; Path=${options.path}`;
47
- if (options.domain) cookie += `; Domain=${options.domain}`;
48
- if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
49
- if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
50
- if (options.secure) cookie += `; Secure`;
51
- if (options.httpOnly) cookie += `; HttpOnly`;
52
- if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
53
- this.headers.append('Set-Cookie', cookie);
54
- }
55
-
56
- deleteCookie(name, options = {}) {
57
- this.setCookie(name, '', { ...options, maxAge: 0, expires: new Date(0) });
58
- }
59
-
60
- text(data, status = this.status, headers = {}) {
61
- const h = this._buildHeaders(headers);
62
- h.set('Content-Type', 'text/plain');
63
- return new Response(data, { status, headers: h });
64
- }
65
-
66
- json(data, status = this.status, headers = {}) {
67
- const h = this._buildHeaders(headers);
68
- h.set('Content-Type', 'application/json');
69
- return new Response(JSON.stringify(data), { status, headers: h });
70
- }
71
-
72
- html(data, status = this.status, headers = {}) {
73
- const h = this._buildHeaders(headers);
74
- h.set('Content-Type', 'text/html');
75
- return new Response(data, { status, headers: h });
76
- }
77
-
78
- _buildHeaders(headers) {
79
- if (Object.keys(headers).length === 0) {
80
- return this.headers;
81
- }
82
- const h = new Headers(this.headers);
83
- for (const key in headers) {
84
- if (Object.prototype.hasOwnProperty.call(headers, key)) {
85
- h.set(key, headers[key]);
86
- }
87
- }
88
- return h;
89
- }
90
- }
91
-
92
- export const Fragment = Symbol('Fragment');
93
-
94
- // ---------- JSX Runtime ----------
95
-
96
- export function jsx(type, props, ...children) {
97
- const normalizedProps = props || {};
98
- const flatChildren = children.flat(Infinity);
99
- return {
100
- type,
101
- props: normalizedProps,
102
- children: flatChildren,
103
- __isJSX: true,
104
- };
105
- }
106
-
107
- // HTML void elements that cannot have children
108
- const VOID_ELEMENTS = new Set([
109
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
110
- 'link', 'meta', 'param', 'source', 'track', 'wbr'
111
- ]);
112
-
113
- // Boolean HTML attributes that should be rendered without a value when true
114
- const BOOLEAN_ATTRIBUTES = new Set([
115
- 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked',
116
- 'controls', 'default', 'defer', 'disabled', 'formnovalidate',
117
- 'hidden', 'inert', 'ismap', 'itemscope', 'loop', 'multiple',
118
- 'muted', 'nomodule', 'novalidate', 'open', 'playsinline',
119
- 'readonly', 'required', 'reversed', 'selected'
120
- ]);
121
-
122
- // CSS properties that do not require a unit when numeric
123
- const UNITLESS_PROPERTIES = new Set([
124
- 'animation-iteration-count', 'border-image-outset', 'border-image-slice',
125
- 'border-image-width', 'box-flex', 'box-flex-group', 'box-ordinal-group',
126
- 'column-count', 'columns', 'flex', 'flex-grow', 'flex-positive',
127
- 'flex-shrink', 'flex-negative', 'flex-order', 'grid-row', 'grid-row-end',
128
- 'grid-row-span', 'grid-row-start', 'grid-column', 'grid-column-end',
129
- 'grid-column-span', 'grid-column-start', 'font-weight', 'line-clamp',
130
- 'line-height', 'opacity', 'order', 'orphans', 'tab-size', 'widows',
131
- 'z-index', 'zoom', 'fill-opacity', 'flood-opacity', 'stop-opacity',
132
- 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit',
133
- 'stroke-opacity', 'stroke-width'
134
- ]);
135
-
136
- function escapeHtml(str) {
137
- const HTML_ESCAPE_MAP = {
138
- '&': '&amp;',
139
- '<': '&lt;',
140
- '>': '&gt;',
141
- '"': '&quot;',
142
- "'": '&#039;'
143
- };
144
- return str.replace(/[&<>"']/g, char => HTML_ESCAPE_MAP[char]);
145
- }
146
-
147
- function camelToKebab(str) {
148
- return str
149
- .replace(/([A-Z])/g, '-$1')
150
- .toLowerCase()
151
- .replace(/^-/, '');
152
- }
153
-
154
- function styleObjectToString(style) {
155
- if (!style || typeof style !== 'object') return '';
156
- const entries = Object.entries(style);
157
- if (entries.length === 0) return '';
158
- return entries
159
- .map(([prop, value]) => {
160
- // Convert camelCase to kebab-case, handling vendor prefixes
161
- let kebabProp = camelToKebab(prop);
162
- // Add px unit for numeric values unless property is unitless
163
- if (typeof value === 'number' && !UNITLESS_PROPERTIES.has(kebabProp)) {
164
- value = `${value}px`;
165
- }
166
- return `${kebabProp}:${value}`;
167
- })
168
- .join(';');
169
- }
170
-
171
- export function renderToString(node) {
172
- if (node == null || typeof node === 'boolean') return '';
173
- if (typeof node === 'string' || typeof node === 'number') {
174
- return escapeHtml(String(node));
175
- }
176
- if (Array.isArray(node)) {
177
- const parts = [];
178
- for (let i = 0; i < node.length; i++) {
179
- parts.push(renderToString(node[i]));
180
- }
181
- return parts.join('');
182
- }
183
- if (!node.__isJSX) {
184
- return escapeHtml(String(node));
185
- }
186
-
187
- const { type, props, children } = node;
188
-
189
- if (type === Fragment) {
190
- const parts = [];
191
- for (let i = 0; i < children.length; i++) {
192
- parts.push(renderToString(children[i]));
193
- }
194
- return parts.join('');
195
- }
196
-
197
- if (typeof type === 'symbol') {
198
- return '';
199
- }
200
-
201
- if (typeof type === 'function') {
202
- const componentResult = type({ ...props, children });
203
- return renderToString(componentResult);
204
- }
205
-
206
- // Handle void elements: they cannot have children
207
- const isVoid = VOID_ELEMENTS.has(type);
208
-
209
- const attrsParts = [];
210
- for (const key in props) {
211
- if (key === 'children' || key === 'key' || key === 'ref') continue;
212
- const value = props[key];
213
-
214
- // Skip null/undefined/false
215
- if (value == null || value === false) continue;
216
-
217
- // dangerouslySetInnerHTML will be handled separately
218
- if (key === 'dangerouslySetInnerHTML') continue;
219
-
220
- // Attribute name mapping
221
- let attrName = key;
222
- if (key === 'className' || key === 'class') {
223
- attrName = 'class';
224
- } else if (key === 'htmlFor' || key === 'for') {
225
- attrName = 'for';
226
- }
227
-
228
- // Boolean attributes: if true, just the attribute name; if false, skip
229
- if (BOOLEAN_ATTRIBUTES.has(attrName)) {
230
- if (value === true) {
231
- attrsParts.push(` ${attrName}`);
232
- }
233
- continue;
234
- }
235
-
236
- // Non-boolean attribute with true -> render as "true"
237
- if (value === true) {
238
- attrsParts.push(` ${attrName}="true"`);
239
- continue;
240
- }
241
-
242
- // Style object handling
243
- if (attrName === 'style' && typeof value === 'object') {
244
- const styleStr = styleObjectToString(value);
245
- if (styleStr) {
246
- attrsParts.push(` style="${escapeHtml(styleStr)}"`);
247
- }
248
- continue;
249
- }
250
-
251
- // Regular attribute
252
- attrsParts.push(` ${attrName}="${escapeHtml(String(value))}"`);
253
- }
254
- const attrs = attrsParts.join('');
255
-
256
- // Handle dangerouslySetInnerHTML
257
- let innerHTML = '';
258
- if (props && props.dangerouslySetInnerHTML && props.dangerouslySetInnerHTML.__html != null) {
259
- innerHTML = props.dangerouslySetInnerHTML.__html;
260
- } else {
261
- const childParts = [];
262
- for (let i = 0; i < children.length; i++) {
263
- childParts.push(renderToString(children[i]));
264
- }
265
- innerHTML = childParts.join('');
266
- }
267
-
268
- if (isVoid) {
269
- return `<${type}${attrs}/>`;
270
- } else {
271
- return `<${type}${attrs}>${innerHTML}</${type}>`;
272
- }
273
- }
274
-
275
- // ---------- Trie-based router for dynamic routes ----------
276
- class TrieNode {
277
- constructor() {
278
- this.children = new Map(); // exact segment -> TrieNode
279
- this.paramChild = null; // node for :param
280
- this.paramName = null; // param name if paramChild exists
281
- this.handler = null; // { handler, options }
282
- }
283
- }
284
-
285
- class RouteTrie {
286
- constructor() {
287
- this.root = new TrieNode();
288
- }
289
-
290
- add(path, handler, options) {
291
- const segments = path.split('/').filter(Boolean);
292
- let node = this.root;
293
- for (const seg of segments) {
294
- if (seg.startsWith(':')) {
295
- if (!node.paramChild) {
296
- node.paramChild = new TrieNode();
297
- node.paramName = seg.slice(1);
298
- }
299
- node = node.paramChild;
300
- } else {
301
- if (!node.children.has(seg)) {
302
- node.children.set(seg, new TrieNode());
303
- }
304
- node = node.children.get(seg);
305
- }
306
- }
307
- node.handler = { handler, options };
308
- }
309
-
310
- match(path) {
311
- const segments = path.split('/').filter(Boolean);
312
- let node = this.root;
313
- const params = {};
314
- for (const seg of segments) {
315
- if (node.children.has(seg)) {
316
- node = node.children.get(seg);
317
- } else if (node.paramChild) {
318
- params[node.paramName] = seg;
319
- node = node.paramChild;
320
- } else {
321
- return null;
322
- }
323
- }
324
- if (node.handler) {
325
- return {
326
- handler: node.handler.handler,
327
- options: node.handler.options,
328
- params
329
- };
330
- }
331
- return null;
332
- }
333
- }
334
-
335
- const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
336
-
337
- export class Edge {
338
- constructor() {
339
- // Static routes: nested map method -> path -> handler
340
- this.staticRoutes = new Map();
341
- for (const m of HTTP_METHODS) {
342
- this.staticRoutes.set(m, new Map());
343
- }
344
- // Dynamic routes per method: RouteTrie
345
- this.dynamicTries = {};
346
- for (const m of HTTP_METHODS) {
347
- this.dynamicTries[m] = new RouteTrie();
348
- }
349
-
350
- this.authKvBinding = 'AUTH_KV';
351
- this.rateLimitKvBinding = 'RATE_LIMIT_KV';
352
- this.defaults = {
353
- cors: { origin: '*', methods: 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD' },
354
- };
355
- this.scheduledHandler = null;
356
- }
357
-
358
- _addRoute(method, path, options, handler) {
359
- if (typeof options === 'boolean') options = { auth: options };
360
- if (typeof options === 'function') {
361
- handler = options;
362
- options = {};
363
- }
364
- if (typeof handler !== 'function') {
365
- throw new Error(`Handler for ${method} ${path} must be a function`);
366
- }
367
-
368
- // Merge with defaults once
369
- const mergedOptions = { ...this.defaults, ...(options || {}) };
370
-
371
- if (!path.includes(':')) {
372
- // Static route (optimized lookup)
373
- const methodMap = this.staticRoutes.get(method);
374
- methodMap.set(path, { handler, options: mergedOptions });
375
- } else {
376
- // Dynamic route
377
- this.dynamicTries[method].add(path, handler, mergedOptions);
378
- }
379
- }
380
-
381
- get(path, options, handler) { this._addRoute('GET', path, options, handler); }
382
- post(path, options, handler) { this._addRoute('POST', path, options, handler); }
383
- put(path, options, handler) { this._addRoute('PUT', path, options, handler); }
384
- delete(path, options, handler) { this._addRoute('DELETE', path, options, handler); }
385
- patch(path, options, handler) { this._addRoute('PATCH', path, options, handler); }
386
- options(path, options, handler) { this._addRoute('OPTIONS', path, options, handler); }
387
- head(path, options, handler) { this._addRoute('HEAD', path, options, handler); }
388
-
389
- scheduled(handler) {
390
- this.scheduledHandler = handler;
391
- }
392
-
393
- async _processAuth(ctx, flag) {
394
- if (!flag) return true;
395
- const token = ctx.getCookie('auth_token') ||
396
- ctx.req.headers.get('Authorization')?.replace(/^Bearer\s+/i, '');
397
- if (!token) return false;
398
-
399
- const kv = ctx.env[this.authKvBinding];
400
- if (!kv) return false;
401
- const data = await kv.get(token);
402
- if (!data) return false;
403
-
404
- if (typeof flag === 'object') {
405
- try {
406
- const payload = JSON.parse(data);
407
- if (flag.role && payload.role !== flag.role) return false;
408
- if (flag.scopes) {
409
- const userScopes = payload.scopes || [];
410
- if (!flag.scopes.every(s => userScopes.includes(s))) return false;
411
- }
412
- } catch {}
413
- }
414
- return true;
415
- }
416
-
417
- async _processRateLimit(ctx, flag) {
418
- if (!flag) return true;
419
- const options = flag === true ? {} : flag;
420
- const { max = 100, window = 60 } = options;
421
- const key = `rl:${ctx.req.headers.get('CF-Connecting-IP') || 'unknown'}`;
422
- const kv = ctx.env[this.rateLimitKvBinding];
423
- if (!kv) return true;
424
- let count = await kv.get(key, 'json') || 0;
425
- if (count >= max) return false;
426
- count++;
427
- await kv.put(key, JSON.stringify(count), { expirationTtl: window });
428
- return true;
429
- }
430
-
431
- _processCors(ctx, flag) {
432
- if (!flag) return;
433
- const opts = typeof flag === 'object' ? flag : this.defaults.cors;
434
- ctx.headers.set('Access-Control-Allow-Origin', opts.origin || '*');
435
- ctx.headers.set('Access-Control-Allow-Methods', opts.methods || 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD');
436
- ctx.headers.set('Access-Control-Allow-Headers', opts.headers || 'Content-Type, Authorization');
437
- ctx.headers.set('Access-Control-Max-Age', '86400');
438
- }
439
-
440
- _processLog(ctx, flag) {
441
- if (!flag) return;
442
- console.log(`${ctx.req.method} ${ctx.req.url} - ${ctx.status}`);
443
- }
444
-
445
- _processCompress(request, response) {
446
- const acceptEncoding = request.headers.get('Accept-Encoding') || '';
447
- let encoding = null;
448
- if (acceptEncoding.includes('gzip')) encoding = 'gzip';
449
- else if (acceptEncoding.includes('deflate')) encoding = 'deflate';
450
- else if (acceptEncoding.includes('br')) encoding = 'br';
451
- if (!encoding || !response.body) return response;
452
-
453
- const stream = response.body.pipeThrough(new CompressionStream(encoding));
454
- const newHeaders = new Headers(response.headers);
455
- newHeaders.set('Content-Encoding', encoding);
456
- newHeaders.set('Vary', 'Accept-Encoding');
457
- return new Response(stream, { status: response.status, statusText: response.statusText, headers: newHeaders });
458
- }
459
-
460
- async _validate(ctx, flag) {
461
- if (!flag) return true;
462
- if (typeof flag === 'function') {
463
- try {
464
- const result = await flag(ctx);
465
- return !!result;
466
- } catch {
467
- return false;
468
- }
469
- }
470
- return true;
471
- }
472
-
473
- async _cacheGet(request) {
474
- const cache = caches.default;
475
- const cached = await cache.match(request);
476
- if (!cached) return null;
477
- return cached;
478
- }
479
-
480
- async _cachePut(request, response, flag) {
481
- if (!flag) return;
482
- const options = flag === true ? {} : flag;
483
- const { ttl = 3600, staleWhileRevalidate = 0 } = options;
484
- const cache = caches.default;
485
- const responseClone = response.clone();
486
- responseClone.headers.set('Cache-Control', `max-age=${ttl}${staleWhileRevalidate > 0 ? `, stale-while-revalidate=${staleWhileRevalidate}` : ''}`);
487
- responseClone.headers.delete('Set-Cookie');
488
- await cache.put(request, responseClone);
489
- }
490
-
491
- async fetch(request, env, executionCtx) {
492
- const url = new URL(request.url);
493
- const path = url.pathname;
494
- const method = request.method;
495
-
496
- // 1. Static route (fast lookup via nested map)
497
- const methodMap = this.staticRoutes.get(method);
498
- if (methodMap) {
499
- const staticRoute = methodMap.get(path);
500
- if (staticRoute) {
501
- return this._handleRoute(staticRoute.handler, staticRoute.options, request, env, executionCtx, {}, url);
502
- }
503
- }
504
-
505
- // 2. Dynamic route via trie
506
- const trie = this.dynamicTries[method];
507
- if (trie) {
508
- const match = trie.match(path);
509
- if (match) {
510
- return this._handleRoute(match.handler, match.options, request, env, executionCtx, match.params, url);
511
- }
512
- }
513
-
514
- return new Response('Not Found', { status: 404 });
515
- }
516
-
517
- async _handleRoute(handler, options, request, env, executionCtx, params, parsedUrl) {
518
- const ctx = new Context(request, env, executionCtx, params, parsedUrl);
519
-
520
- if (!(await this._validate(ctx, options.validate))) {
521
- return ctx.text('Validation failed', 400);
522
- }
523
-
524
- if (!(await this._processAuth(ctx, options.auth))) {
525
- return ctx.text('Unauthorized', 401);
526
- }
527
-
528
- if (!(await this._processRateLimit(ctx, options.rateLimit))) {
529
- return ctx.text('Too Many Requests', 429);
530
- }
531
-
532
- // Caching (GET only)
533
- let cachedResponse = null;
534
- if (options.cache && request.method === 'GET') {
535
- cachedResponse = await this._cacheGet(request);
536
- if (cachedResponse) {
537
- this._processCors(ctx, options.cors);
538
- this._processLog(ctx, options.log);
539
- return cachedResponse;
540
- }
541
- }
542
-
543
- this._processCors(ctx, options.cors);
544
-
545
- let response;
546
- try {
547
- const result = await handler(ctx);
548
- if (result && result.__isJSX) {
549
- response = ctx.html(renderToString(result));
550
- } else {
551
- response = result instanceof Response ? result : ctx.text('OK');
552
- }
553
- ctx.status = response.status;
554
- } catch (err) {
555
- console.error(err);
556
- response = ctx.text('Internal Server Error', 500);
557
- ctx.status = 500;
558
- }
559
-
560
- // Cache successful GET responses
561
- if (options.cache && request.method === 'GET' && response.status === 200) {
562
- ctx.executionCtx.waitUntil(this._cachePut(request, response.clone(), options.cache));
563
- }
564
-
565
- if (options.compress) {
566
- response = this._processCompress(request, response);
567
- }
568
-
569
- this._processLog(ctx, options.log);
570
-
571
- return response;
572
- }
573
- }
File without changes
File without changes