@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.
- package/dist/edge-client.js +1 -0
- package/{edge-server.d.ts → dist/edge-server.d.ts} +6 -12
- package/dist/edge-server.js +1 -0
- package/package.json +25 -18
- package/edge-client.js +0 -287
- package/edge-server.js +0 -573
- /package/{edge-client.d.ts → dist/edge-client.d.ts} +0 -0
- /package/{README.md → readme.md} +0 -0
|
@@ -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
|
-
|
|
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 |
|
|
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
|
-
|
|
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={"&":"&","<":"<",">":">",'"':""","'":"'"},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.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Edge framework used by Lengkapp",
|
|
5
|
-
"main": "edge-server.js",
|
|
6
|
-
"types": "
|
|
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
|
-
"
|
|
25
|
-
"edge-client.d.ts",
|
|
26
|
-
"edge-server.js",
|
|
27
|
-
"edge-server.d.ts"
|
|
24
|
+
"dist"
|
|
28
25
|
],
|
|
29
26
|
"scripts": {
|
|
30
|
-
"
|
|
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": [
|
|
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
|
-
'&': '&',
|
|
139
|
-
'<': '<',
|
|
140
|
-
'>': '>',
|
|
141
|
-
'"': '"',
|
|
142
|
-
"'": '''
|
|
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
|
/package/{README.md → readme.md}
RENAMED
|
File without changes
|