@lengkapp/edge 0.0.6 → 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.6",
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,306 +0,0 @@
1
- (function () {
2
- if (window.__edge) return;
3
- window.__edge = true;
4
-
5
- var d = document, head = d.head, root = d.documentElement;
6
-
7
- var METHODS = ['get', 'post', 'put', 'patch', 'delete'];
8
- var METHOD_ATTRS = METHODS.map(function (m) { return '_' + m; });
9
- var HAS_BODY = { post: 1, put: 1, patch: 1 };
10
- var SEL = METHOD_ATTRS.map(function (a) { return '[' + a + ']'; }).join(',');
11
-
12
- var DELEGATABLE = new Set([
13
- 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
14
- 'mousemove', 'contextmenu', 'focusin', 'focusout', 'keydown', 'keyup',
15
- 'keypress', 'change', 'input', 'submit', 'reset', 'wheel',
16
- 'touchstart', 'touchend', 'touchmove'
17
- ]);
18
-
19
- var bound = new WeakSet();
20
- var running = new WeakSet();
21
- var targetCache = new WeakMap();
22
- var wasVisible = new WeakMap();
23
- var ioObservers = new Map();
24
- var injected = new Set();
25
- var executedSrc = new Set();
26
-
27
- var style = d.createElement('style');
28
- style.textContent =
29
- '.aj-skel-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;' +
30
- 'max-width:100%;max-height:600px;display:flex;align-items:center;justify-content:center}' +
31
- '.aj-skel-wrap svg{width:1.75em;height:1.75em;color:#c7c7c7;' +
32
- 'animation:aj-pulse 1.4s ease-in-out infinite}' +
33
- '@keyframes aj-pulse{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:1;transform:scale(1)}}' +
34
- '.aj-retry-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;' +
35
- 'max-height:600px;display:flex;align-items:center;justify-content:center}' +
36
- '.aj-retry{width:2.5em;height:2.5em;padding:0;cursor:pointer;background:#007bff;color:#fff;' +
37
- 'border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;' +
38
- 'transition:background .15s ease}' +
39
- '.aj-retry:hover{background:#0056b3}' +
40
- '.aj-retry:hover svg{transform:rotate(-90deg)}' +
41
- '.aj-retry svg{width:1.25em;height:1.25em;transition:transform .3s ease}';
42
- head.appendChild(style);
43
-
44
- function cfg(el) {
45
- var attr = null;
46
- for (var i = 0; i < METHOD_ATTRS.length; i++) {
47
- if (el.hasAttribute(METHOD_ATTRS[i])) { attr = METHOD_ATTRS[i]; break; }
48
- }
49
- if (!attr) return null;
50
- var method = attr.slice(1);
51
- return {
52
- url: el.getAttribute(attr),
53
- method: method.toUpperCase(),
54
- hasBody: !!HAS_BODY[method],
55
- target: el.getAttribute('_target'),
56
- trigger: el.getAttribute('_trigger'),
57
- form: el.getAttribute('_form'),
58
- json: el.getAttribute('_json'),
59
- skeleton: el.getAttribute('_skeleton') !== 'false',
60
- retry: el.getAttribute('_retry') !== 'false'
61
- };
62
- }
63
-
64
- function resolveTarget(el, c) {
65
- if (!c.target || c.target === 'this') return el;
66
- var cached = targetCache.get(el);
67
- if (cached && cached.isConnected) return cached;
68
- var found = d.querySelector(c.target);
69
- if (found) targetCache.set(el, found);
70
- return found;
71
- }
72
-
73
- function triggersFor(c) {
74
- return (c.trigger || (c.target === 'this' ? 'load' : 'click'))
75
- .split(',').map(function (s) { return s.trim().toLowerCase(); });
76
- }
77
-
78
- function isActuallyVisible(el) {
79
- var s = getComputedStyle(el);
80
- if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
81
- var r = el.getBoundingClientRect();
82
- return r.width > 0 && r.height > 0;
83
- }
84
-
85
- var SKELETON_ICON =
86
- '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" ' +
87
- 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
88
- 'stroke-linejoin="round" aria-hidden="true">' +
89
- '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>' +
90
- '</svg>';
91
-
92
- function showSkeleton(el) {
93
- el.innerHTML = '<div class="aj-skel-wrap">' + SKELETON_ICON + '</div>';
94
- }
95
-
96
- var RETRY_ICON =
97
- '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" ' +
98
- 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
99
- 'stroke-linejoin="round" aria-hidden="true">' +
100
- '<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"/>' +
101
- '</svg>';
102
-
103
- function showRetry(el, fn) {
104
- el.innerHTML = '';
105
- var wrap = d.createElement('div');
106
- wrap.className = 'aj-retry-wrap';
107
- var b = d.createElement('button');
108
- b.type = 'button';
109
- b.className = 'aj-retry';
110
- b.setAttribute('aria-label', 'Retry');
111
- b.innerHTML = RETRY_ICON;
112
- b.addEventListener('click', fn, { once: true });
113
- wrap.appendChild(b);
114
- el.appendChild(wrap);
115
- }
116
-
117
- function parseList(raw) {
118
- if (!raw) return [];
119
- return raw.replace(/^\[|\]$/g, '').split(',')
120
- .map(function (s) { return s.trim().replace(/^['"]|['"]$/g, ''); })
121
- .filter(Boolean);
122
- }
123
-
124
- function injectResources(headers) {
125
- parseList(headers.get('x-css-required')).forEach(function (url) {
126
- if (injected.has(url)) return;
127
- injected.add(url);
128
- var link = d.createElement('link');
129
- link.rel = 'stylesheet'; link.href = url;
130
- head.appendChild(link);
131
- });
132
- parseList(headers.get('x-js-required')).forEach(function (url) {
133
- if (injected.has(url)) return;
134
- injected.add(url);
135
- var s = d.createElement('script');
136
- s.src = url;
137
- head.appendChild(s);
138
- });
139
- }
140
-
141
- function execScripts(container) {
142
- var scripts = Array.prototype.slice.call(container.querySelectorAll('script'));
143
- var i = 0;
144
- function next() {
145
- if (i >= scripts.length) return;
146
- var old = scripts[i];
147
- var s = d.createElement('script');
148
- for (var a = 0; a < old.attributes.length; a++) {
149
- s.setAttribute(old.attributes[a].name, old.attributes[a].value);
150
- }
151
- if (old.nonce) s.nonce = old.nonce;
152
- if (old.src) {
153
- if (executedSrc.has(old.src)) { old.remove(); i++; next(); return; }
154
- executedSrc.add(old.src);
155
- s.onload = s.onerror = function () { i++; next(); };
156
- old.replaceWith(s);
157
- } else {
158
- s.textContent = '{\n' + old.textContent + '\n}';
159
- old.replaceWith(s);
160
- i++; next();
161
- }
162
- }
163
- next();
164
- }
165
-
166
- function setContent(el, html) {
167
- el.innerHTML = html;
168
- execScripts(el);
169
- bind(el);
170
- }
171
-
172
- function doFetch(el, c) {
173
- if (running.has(el)) return;
174
- running.add(el);
175
-
176
- var target = resolveTarget(el, c);
177
- if (!target) { running.delete(el); return; }
178
- if (c.skeleton) showSkeleton(target);
179
-
180
- var opts = { method: c.method };
181
- if (c.hasBody) {
182
- if (c.json) {
183
- var obj = {};
184
- c.json.split(',').forEach(function (name) {
185
- name = name.trim();
186
- var input = d.querySelector('[name="' + name + '"]');
187
- obj[name] = input ? input.value : undefined;
188
- });
189
- opts.headers = { 'Content-Type': 'application/json' };
190
- opts.body = JSON.stringify(obj);
191
- } else if (c.form) {
192
- var form = d.getElementById(c.form);
193
- opts.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
194
- opts.body = form ? new URLSearchParams(new FormData(form)).toString() : '';
195
- }
196
- }
197
-
198
- fetch(c.url, opts)
199
- .then(function (res) {
200
- if (!res.ok) throw new Error('HTTP ' + res.status);
201
- injectResources(res.headers);
202
- return res.text();
203
- })
204
- .then(function (html) { setContent(target, html); })
205
- .catch(function (err) {
206
- console.error(err);
207
- if (c.retry) showRetry(target, function () { doFetch(el, c); });
208
- })
209
- .finally(function () { running.delete(el); });
210
- }
211
-
212
- var delegationInit = false;
213
- function initDelegation() {
214
- if (delegationInit) return;
215
- delegationInit = true;
216
- DELEGATABLE.forEach(function (evt) {
217
- var isClickOrSubmit = evt === 'click' || evt === 'submit';
218
- d.addEventListener(evt, function (e) {
219
- var t = e.target;
220
- if (!(t instanceof Element)) return;
221
- var el = t.closest(SEL);
222
- if (!el) return;
223
- var c = cfg(el);
224
- if (!c || triggersFor(c).indexOf(evt) === -1) return;
225
- if (isClickOrSubmit) e.preventDefault();
226
- doFetch(el, c);
227
- }, isClickOrSubmit ? false : { passive: true });
228
- });
229
- }
230
-
231
- function bindOne(el) {
232
- if (bound.has(el)) return;
233
- bound.add(el);
234
- var c = cfg(el);
235
- if (!c) return;
236
-
237
- triggersFor(c).forEach(function (trig) {
238
- if (trig === 'load' || trig === 'domcontentloaded' || trig === 'ready') {
239
- queueMicrotask(function () { doFetch(el, c); });
240
- } else if (trig === 'visible' || trig === 'intersect') {
241
- wasVisible.set(el, false);
242
- var obs = new IntersectionObserver(function (entries) {
243
- entries.forEach(function (entry) {
244
- if (!entry.target.isConnected) { obs.disconnect(); ioObservers.delete(el); return; }
245
- var vis = entry.isIntersecting && isActuallyVisible(el);
246
- if (vis && !wasVisible.get(el)) doFetch(el, c);
247
- wasVisible.set(el, vis);
248
- });
249
- });
250
- obs.observe(el);
251
- ioObservers.set(el, obs);
252
- } else if (!DELEGATABLE.has(trig)) {
253
- el.addEventListener(trig, function () { doFetch(el, c); });
254
- }
255
- });
256
- }
257
-
258
- function bind(subtreeRoot) {
259
- if (subtreeRoot.nodeType !== 1) return;
260
- if (subtreeRoot.matches && subtreeRoot.matches(SEL)) bindOne(subtreeRoot);
261
- if (subtreeRoot.querySelectorAll) subtreeRoot.querySelectorAll(SEL).forEach(bindOne);
262
- }
263
-
264
- function cleanup(removedRoot) {
265
- if (!ioObservers.size) return;
266
- ioObservers.forEach(function (obs, el) {
267
- if (removedRoot === el || removedRoot.contains(el)) {
268
- obs.disconnect();
269
- ioObservers.delete(el);
270
- bound.delete(el);
271
- }
272
- });
273
- }
274
-
275
- var addQueue = new Set(), removeQueue = new Set(), flushScheduled = false;
276
- function flush() {
277
- flushScheduled = false;
278
- var adds = Array.from(addQueue); addQueue.clear();
279
- var rems = Array.from(removeQueue); removeQueue.clear();
280
- adds.forEach(bind);
281
- rems.forEach(cleanup);
282
- }
283
- function scheduleFlush() {
284
- if (flushScheduled) return;
285
- flushScheduled = true;
286
- queueMicrotask(flush);
287
- }
288
-
289
- new MutationObserver(function (mutations) {
290
- mutations.forEach(function (m) {
291
- m.addedNodes.forEach(function (n) { if (n.nodeType === 1) addQueue.add(n); });
292
- m.removedNodes.forEach(function (n) { if (n.nodeType === 1) removeQueue.add(n); });
293
- });
294
- scheduleFlush();
295
- }).observe(root, { childList: true, subtree: true });
296
-
297
- function initAll() {
298
- initDelegation();
299
- bind(root);
300
- }
301
- if (d.readyState === 'loading') {
302
- d.addEventListener('DOMContentLoaded', initAll, { once: true });
303
- } else {
304
- initAll();
305
- }
306
- })();
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