@lengkapp/edge 0.0.4 → 0.0.5

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/edge-client.js CHANGED
@@ -1,279 +1,287 @@
1
1
  (() => {
2
- 'use strict';
2
+ 'use strict';
3
3
 
4
- const d = document,
5
- body = d.body,
6
- head = d.head;
4
+ const d = document,
5
+ body = d.body,
6
+ head = d.head;
7
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
- ]);
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
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
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
+ }
24
49
 
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 { display:flex; flex-direction:column; gap:8px; padding:10px; }
32
- .df-skeleton .df-bar {
33
- height:12px; background:linear-gradient(90deg,#e0e0e0 25%,#f0f0f0 50%,#e0e0e0 75%);
34
- background-size:200% 100%; animation:df-shimmer 1.5s infinite; border-radius:4px;
35
- }
36
- @keyframes df-shimmer { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
37
50
  .df-retry {
38
51
  display:inline-block; padding:8px 16px; background:#007bff; color:#fff;
39
52
  border-radius:4px; cursor:pointer; text-decoration:none; font-size:14px;
40
53
  }
41
54
  .df-retry:hover { background:#0056b3; }
42
55
  `;
43
- head.appendChild(style);
44
- };
45
- injectBaseStyles();
56
+ head.appendChild(style);
57
+ };
58
+ injectBaseStyles();
46
59
 
47
- // ---------- Pre‑built templates (cloned on use) ----------
48
- const skeletonTemplate = d.createElement('template');
49
- skeletonTemplate.innerHTML = `
50
- <div class="df-skeleton">
51
- <div class="df-bar"></div>
52
- <div class="df-bar"></div>
53
- <div class="df-bar"></div>
54
- </div>`;
55
- const retryTemplate = d.createElement('template');
56
- retryTemplate.innerHTML = `<span class="df-retry">Retry</span>`;
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>`;
57
66
 
58
- // ---------- Resource helpers ----------
59
- const parseHeaderList = (str) => {
60
- if (!str) return [];
61
- return str.replace(/^\[|\]$/g, '').split(',')
62
- .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
63
- .filter(Boolean);
64
- };
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
+ };
65
74
 
66
- const injectResources = (resources, type) => {
67
- resources.forEach(url => {
68
- if (resourceCache.has(url)) return;
69
- resourceCache.add(url);
70
- if (type === 'css') {
71
- const link = d.createElement('link');
72
- link.rel = 'stylesheet'; link.href = url;
73
- head.appendChild(link);
74
- } else if (type === 'js') {
75
- const script = d.createElement('script');
76
- script.src = url;
77
- body.appendChild(script);
78
- }
79
- });
80
- };
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
+ };
81
90
 
82
- // ---------- Visibility helpers ----------
83
- const isElementActuallyVisible = (el) => {
84
- const style = getComputedStyle(el);
85
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
86
- const rect = el.getBoundingClientRect();
87
- return rect.width > 0 && rect.height > 0;
88
- };
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
+ };
89
98
 
90
- // Shared IntersectionObserver
91
- let intersectionObserver = null;
92
- const getIO = () => {
93
- if (!intersectionObserver) {
94
- intersectionObserver = new IntersectionObserver((entries) => {
95
- entries.forEach(entry => {
96
- const el = entry.target;
97
- const visible = entry.isIntersecting && isElementActuallyVisible(el);
98
- if (visible && el.dataset.wasVisible !== 'true') {
99
- run(el);
100
- }
101
- el.dataset.wasVisible = visible ? 'true' : 'false';
102
- });
103
- }, { threshold: 0 });
104
- }
105
- return intersectionObserver;
106
- };
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
+ };
107
116
 
108
- // ---------- Core run function ----------
109
- const run = async (el) => {
110
- if (el.dataset.running === 'true') return;
111
- el.dataset.running = 'true';
117
+ // ---------- Core run function ----------
118
+ const run = async (el) => {
119
+ if (el.dataset.running === 'true') return;
120
+ el.dataset.running = 'true';
112
121
 
113
- const post = el.hasAttribute('_post');
114
- const url = el.getAttribute('_post') || el.getAttribute('_get');
115
- let bodyData, headers = {};
122
+ const post = el.hasAttribute('_post');
123
+ const url = el.getAttribute('_post') || el.getAttribute('_get');
124
+ let bodyData, headers = {};
116
125
 
117
- if (post) {
118
- const formId = el.getAttribute('_form');
119
- const json = el.getAttribute('_json');
120
- if (formId) {
121
- const form = d.getElementById(formId);
122
- if (form) bodyData = new URLSearchParams(new FormData(form));
123
- } else if (json) {
124
- bodyData = JSON.stringify(Object.fromEntries(
125
- json.split(',').map(name => {
126
- const input = d.querySelector(`[name="${name}"]`);
127
- return [name, input ? input.value : ''];
128
- })
129
- ));
130
- headers['Content-Type'] = 'application/json';
131
- }
132
- }
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
+ }
133
142
 
134
- // Resolve target (cached)
135
- const targetSelector = el.getAttribute('_target');
136
- let container = targetCache.get(el);
137
- if (!container) {
138
- container = targetSelector === 'this' ? el : d.querySelector(targetSelector);
139
- if (container) targetCache.set(el, container);
140
- }
141
- if (!container) {
142
- console.warn('Target not found:', targetSelector);
143
- el.dataset.running = 'false';
144
- return;
145
- }
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
+ }
146
155
 
147
- // Show skeleton (clone template)
148
- container.replaceChildren(skeletonTemplate.content.cloneNode(true));
156
+ container.replaceChildren(skeletonTemplate.content.cloneNode(true));
149
157
 
150
- try {
151
- const res = await fetch(url, { method: post ? 'POST' : 'GET', body: bodyData, headers });
158
+ try {
159
+ const res = await fetch(url, { method: post ? 'POST' : 'GET', body: bodyData, headers });
152
160
 
153
- // Inject resources from headers
154
- const css = res.headers.get('x-css-required') || res.headers.get('x-css-requiered');
155
- const js = res.headers.get('x-js-required');
156
- if (css) injectResources(parseHeaderList(css), 'css');
157
- if (js) injectResources(parseHeaderList(js), 'js');
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');
158
166
 
159
- const text = await res.text();
160
- container.innerHTML = text; // MutationObserver picks up new declarative elements
161
- } catch (err) {
162
- console.error(err);
163
- // Show retry (clone template)
164
- const retry = retryTemplate.content.firstElementChild.cloneNode(true);
165
- retry.addEventListener('click', (e) => {
166
- e.preventDefault();
167
- e.stopPropagation();
168
- run(el);
169
- });
170
- container.replaceChildren(retry);
171
- } finally {
172
- el.dataset.running = 'false';
173
- }
174
- };
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
+ };
175
183
 
176
- // ---------- Trigger parsing and caching ----------
177
- const getTriggerEvents = (el) => {
178
- let events = triggerCache.get(el);
179
- if (events) return events;
180
- events = new Set();
181
- const attr = el.getAttribute('_trigger');
182
- if (attr && attr.trim() !== '') {
183
- attr.split(',').forEach(s => {
184
- const evt = s.trim().toLowerCase();
185
- if (VALID_EVENTS.has(evt)) events.add(evt);
186
- });
187
- } else {
188
- // Default: click (plus load if _target="this")
189
- events.add('click');
190
- if (el.getAttribute('_target') === 'this') events.add('load');
191
- }
192
- triggerCache.set(el, events);
193
- return events;
194
- };
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
+ };
195
203
 
196
- // ---------- Element initialisation (no per‑element listeners) ----------
197
- const initElement = (el) => {
198
- if (el.dataset.initialized === 'true') return;
199
- el.dataset.initialized = 'true';
204
+ // ---------- Element initialisation (no per‑element listeners) ----------
205
+ const initElement = (el) => {
206
+ if (el.dataset.initialized === 'true') return;
207
+ el.dataset.initialized = 'true';
200
208
 
201
- const events = getTriggerEvents(el);
209
+ const events = getTriggerEvents(el);
202
210
 
203
- // Handle load / ready immediately
204
- if (events.has('load') || events.has('domcontentloaded') || events.has('ready')) {
205
- run(el);
206
- }
207
- // Set up visibility observation
208
- if (events.has('visible') || events.has('intersect')) {
209
- el.dataset.wasVisible = 'false';
210
- getIO().observe(el);
211
- }
212
- // All other events are handled by the global delegated listener
213
- };
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
+ };
214
222
 
215
- // ---------- Global event delegation ----------
216
- const delegatedEvents = new Set([
217
- 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
218
- 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu',
219
- 'focus', 'blur', 'focusin', 'focusout',
220
- 'keydown', 'keyup', 'keypress',
221
- 'change', 'input', 'submit', 'reset',
222
- 'scroll', 'resize', 'wheel', 'touchstart', 'touchend', 'touchmove'
223
- ]);
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
+ ]);
224
232
 
225
- // Attach one listener per event type, passive for scroll‑like events
226
- delegatedEvents.forEach(evt => {
227
- const isPassive = ['scroll', 'touchstart', 'touchmove', 'touchend', 'wheel'].includes(evt);
228
- d.addEventListener(evt, (e) => {
229
- const target = e.target;
230
- if (!(target instanceof Element)) return;
231
- const el = target.closest('[_get], [_post]');
232
- if (!el) return;
233
- const events = getTriggerEvents(el);
234
- if (events.has(evt)) {
235
- if (evt === 'click') e.preventDefault();
236
- run(el);
237
- }
238
- }, isPassive ? { passive: true } : false);
239
- });
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
+ });
240
248
 
241
- // ---------- MutationObserver (batch processing) ----------
242
- let mutationQueue = [];
243
- let mutationScheduled = false;
249
+ // ---------- MutationObserver (batch processing) ----------
250
+ let mutationQueue = [];
251
+ let mutationScheduled = false;
244
252
 
245
- const processMutations = () => {
246
- mutationScheduled = false;
247
- const nodes = mutationQueue;
248
- mutationQueue = [];
249
- nodes.forEach(node => {
250
- if (node.nodeType !== 1) return;
251
- if (node.matches('[_get], [_post]')) initElement(node);
252
- node.querySelectorAll('[_get], [_post]').forEach(initElement);
253
- });
254
- };
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
+ };
255
263
 
256
- const mo = new MutationObserver((mutations) => {
257
- mutations.forEach(m => {
258
- m.addedNodes.forEach(node => {
259
- if (node.nodeType === 1) mutationQueue.push(node);
260
- });
261
- });
262
- if (!mutationScheduled) {
263
- mutationScheduled = true;
264
- Promise.resolve().then(processMutations);
265
- }
264
+ const mo = new MutationObserver((mutations) => {
265
+ mutations.forEach(m => {
266
+ m.addedNodes.forEach(node => {
267
+ if (node.nodeType === 1) mutationQueue.push(node);
266
268
  });
267
- mo.observe(body, { childList: true, subtree: true });
269
+ });
270
+ if (!mutationScheduled) {
271
+ mutationScheduled = true;
272
+ Promise.resolve().then(processMutations);
273
+ }
274
+ });
275
+ mo.observe(body, { childList: true, subtree: true });
268
276
 
269
- // ---------- Initialisation on DOM ready ----------
270
- const initAll = () => {
271
- d.querySelectorAll('[_get], [_post]').forEach(initElement);
272
- };
277
+ // ---------- Initialisation on DOM ready ----------
278
+ const initAll = () => {
279
+ d.querySelectorAll('[_get], [_post]').forEach(initElement);
280
+ };
273
281
 
274
- if (d.readyState === 'loading') {
275
- d.addEventListener('DOMContentLoaded', initAll, { once: true });
276
- } else {
277
- initAll();
278
- }
279
- })();
282
+ if (d.readyState === 'loading') {
283
+ d.addEventListener('DOMContentLoaded', initAll, { once: true });
284
+ } else {
285
+ initAll();
286
+ }
287
+ })();
package/edge-server.d.ts CHANGED
@@ -1,83 +1,148 @@
1
- export const Fragment: unique symbol;
1
+ // Type definitions for edge-server
2
2
 
3
- export interface JSXNode {
4
- type: any;
5
- props: Record<string, any>;
6
- children: any[];
7
- __isJSX: true;
8
- }
9
-
10
- export function jsx(
11
- type: any,
12
- props?: Record<string, any> | null,
13
- ...children: any[]
14
- ): JSXNode;
15
-
16
- export function renderToString(node: any): string;
17
-
18
- export interface RouteOptions {
19
- auth?: boolean | { role?: string; scopes?: string[] };
20
- rateLimit?: boolean | { max?: number; window?: number };
21
- cors?: boolean | { origin?: string; methods?: string; headers?: string };
22
- validate?: (ctx: Context) => boolean | Promise<boolean>;
23
- log?: boolean;
24
- cache?: boolean | { ttl?: number; staleWhileRevalidate?: number };
25
- compress?: boolean;
26
- }
3
+ /// <reference lib="dom" />
4
+ /// <reference types="@cloudflare/workers-types" />
27
5
 
28
6
  export class Context {
7
+ constructor(
8
+ request: Request,
9
+ env: any,
10
+ executionCtx: ExecutionContext,
11
+ params?: Record<string, string>,
12
+ parsedUrl?: URL | null
13
+ );
14
+
29
15
  req: Request;
30
16
  env: any;
31
17
  executionCtx: ExecutionContext;
32
18
  params: Record<string, string>;
33
19
  status: number;
34
20
  headers: Headers;
21
+ url: URL;
35
22
 
36
- constructor(
37
- request: Request,
38
- env: any,
39
- executionCtx: ExecutionContext,
40
- params?: Record<string, string>,
41
- parsedUrl?: URL
42
- );
23
+ // Private members (not accessible, but present)
24
+ private _rawCookie: string;
25
+ private _cookies: Record<string, string> | null;
43
26
 
44
27
  getCookie(name: string): string | null;
45
- get query(): URLSearchParams;
46
- setCookie(name: string, value: string, options?: Record<string, any>): void;
47
- deleteCookie(name: string, options?: Record<string, any>): void;
28
+ readonly query: URLSearchParams;
29
+ setCookie(
30
+ name: string,
31
+ value: string,
32
+ options?: {
33
+ path?: string;
34
+ domain?: string;
35
+ maxAge?: number;
36
+ expires?: Date;
37
+ secure?: boolean;
38
+ httpOnly?: boolean;
39
+ sameSite?: 'Strict' | 'Lax' | 'None';
40
+ }
41
+ ): void;
42
+ deleteCookie(
43
+ name: string,
44
+ options?: {
45
+ path?: string;
46
+ domain?: string;
47
+ secure?: boolean;
48
+ httpOnly?: boolean;
49
+ sameSite?: 'Strict' | 'Lax' | 'None';
50
+ }
51
+ ): void;
52
+
48
53
  text(data: string, status?: number, headers?: Record<string, string>): Response;
49
54
  json(data: any, status?: number, headers?: Record<string, string>): Response;
50
55
  html(data: string, status?: number, headers?: Record<string, string>): Response;
56
+
57
+ private _buildHeaders(headers: Record<string, string>): Headers;
58
+ }
59
+
60
+ export const Fragment: unique symbol;
61
+
62
+ export interface JSXElement {
63
+ type: any;
64
+ props: Record<string, any>;
65
+ children: any[];
66
+ __isJSX: true;
67
+ }
68
+
69
+ export function jsx(type: any, props: Record<string, any> | null, ...children: any[]): JSXElement;
70
+
71
+ export function renderToString(node: any): string;
72
+
73
+ // Route option interfaces
74
+
75
+ interface AuthOptions {
76
+ role?: string;
77
+ scopes?: string[];
78
+ }
79
+
80
+ interface RateLimitOptions {
81
+ max?: number;
82
+ window?: number;
83
+ }
84
+
85
+ interface CorsOptions {
86
+ origin?: string;
87
+ methods?: string;
88
+ headers?: string;
89
+ credentials?: boolean;
90
+ }
91
+
92
+ interface CacheOptions {
93
+ ttl?: number;
94
+ staleWhileRevalidate?: number;
95
+ }
96
+
97
+ interface RouteOptions {
98
+ auth?: boolean | AuthOptions;
99
+ rateLimit?: boolean | RateLimitOptions;
100
+ cors?: boolean | CorsOptions;
101
+ validate?: (ctx: Context) => boolean | Promise<boolean>;
102
+ cache?: boolean | CacheOptions;
103
+ compress?: boolean;
104
+ log?: boolean;
51
105
  }
52
106
 
107
+ type RouteHandler = (ctx: Context) => Response | JSXElement | any | Promise<Response | JSXElement | any>;
108
+
53
109
  export class Edge {
54
110
  constructor();
55
- authKvBinding: string;
56
- rateLimitKvBinding: string;
57
- defaults: {
58
- cors: { origin: string; methods: string };
59
- };
60
- scheduledHandler: ((...args: any[]) => void) | null;
61
-
62
- get(path: string, handler: (ctx: Context) => any): void;
63
- get(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
64
- post(path: string, handler: (ctx: Context) => any): void;
65
- post(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
66
- put(path: string, handler: (ctx: Context) => any): void;
67
- put(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
68
- delete(path: string, handler: (ctx: Context) => any): void;
69
- delete(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
70
- patch(path: string, handler: (ctx: Context) => any): void;
71
- patch(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
72
- options(path: string, handler: (ctx: Context) => any): void;
73
- options(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
74
- head(path: string, handler: (ctx: Context) => any): void;
75
- head(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
76
- scheduled(handler: (...args: any[]) => void): void;
77
-
78
- fetch(
79
- request: Request,
80
- env: any,
81
- executionCtx: ExecutionContext
82
- ): Promise<Response>;
111
+
112
+ // Route registration methods
113
+ get(path: string, handler: RouteHandler): void;
114
+ get(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
115
+
116
+ post(path: string, handler: RouteHandler): void;
117
+ post(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
118
+
119
+ put(path: string, handler: RouteHandler): void;
120
+ put(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
121
+
122
+ delete(path: string, handler: RouteHandler): void;
123
+ delete(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
124
+
125
+ patch(path: string, handler: RouteHandler): void;
126
+ patch(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
127
+
128
+ options(path: string, handler: RouteHandler): void;
129
+ options(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
130
+
131
+ head(path: string, handler: RouteHandler): void;
132
+ head(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
133
+
134
+ scheduled(handler: (controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>): void;
135
+
136
+ // Main fetch handler
137
+ fetch(request: Request, env: any, executionCtx: ExecutionContext): Promise<Response>;
138
+ }
139
+
140
+ // JSX namespace support
141
+ declare global {
142
+ namespace JSX {
143
+ interface Element extends JSXElement {}
144
+ interface IntrinsicElements {
145
+ [elemName: string]: any;
146
+ }
147
+ }
83
148
  }
package/edge-server.js CHANGED
@@ -91,6 +91,8 @@ class Context {
91
91
 
92
92
  export const Fragment = Symbol('Fragment');
93
93
 
94
+ // ---------- JSX Runtime ----------
95
+
94
96
  export function jsx(type, props, ...children) {
95
97
  const normalizedProps = props || {};
96
98
  const flatChildren = children.flat(Infinity);
@@ -102,6 +104,70 @@ export function jsx(type, props, ...children) {
102
104
  };
103
105
  }
104
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
+
105
171
  export function renderToString(node) {
106
172
  if (node == null || typeof node === 'boolean') return '';
107
173
  if (typeof node === 'string' || typeof node === 'number') {
@@ -137,53 +203,73 @@ export function renderToString(node) {
137
203
  return renderToString(componentResult);
138
204
  }
139
205
 
206
+ // Handle void elements: they cannot have children
207
+ const isVoid = VOID_ELEMENTS.has(type);
208
+
140
209
  const attrsParts = [];
141
210
  for (const key in props) {
142
- if (key === 'children') continue;
211
+ if (key === 'children' || key === 'key' || key === 'ref') continue;
143
212
  const value = props[key];
213
+
214
+ // Skip null/undefined/false
144
215
  if (value == null || value === false) continue;
145
- if (key === 'className') {
146
- attrsParts.push(` class="${escapeHtml(value)}"`);
147
- } else if (key === 'htmlFor') {
148
- attrsParts.push(` for="${escapeHtml(value)}"`);
149
- } else if (key.startsWith('on') && typeof value === 'function') {
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
+ }
150
233
  continue;
151
- } else if (key === 'style' && typeof value === 'object') {
152
- const styleStr = Object.entries(value)
153
- .map(([prop, val]) => `${camelToKebab(prop)}:${val}`)
154
- .join(';');
155
- attrsParts.push(` style="${escapeHtml(styleStr)}"`);
156
- } else if (value === true) {
157
- attrsParts.push(` ${key}`);
158
- } else {
159
- attrsParts.push(` ${key}="${escapeHtml(String(value))}"`);
160
234
  }
161
- }
162
- const attrs = attrsParts.join('');
163
235
 
164
- const childParts = [];
165
- for (let i = 0; i < children.length; i++) {
166
- childParts.push(renderToString(children[i]));
167
- }
168
- const innerHTML = childParts.join('');
236
+ // Non-boolean attribute with true -> render as "true"
237
+ if (value === true) {
238
+ attrsParts.push(` ${attrName}="true"`);
239
+ continue;
240
+ }
169
241
 
170
- return `<${type}${attrs}>${innerHTML}</${type}>`;
171
- }
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
+ }
172
250
 
173
- const HTML_ESCAPE_MAP = {
174
- '&': '&amp;',
175
- '<': '&lt;',
176
- '>': '&gt;',
177
- '"': '&quot;',
178
- "'": '&#039;'
179
- };
251
+ // Regular attribute
252
+ attrsParts.push(` ${attrName}="${escapeHtml(String(value))}"`);
253
+ }
254
+ const attrs = attrsParts.join('');
180
255
 
181
- function escapeHtml(str) {
182
- return str.replace(/[&<>"']/g, char => HTML_ESCAPE_MAP[char]);
183
- }
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
+ }
184
267
 
185
- function camelToKebab(str) {
186
- return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
268
+ if (isVoid) {
269
+ return `<${type}${attrs}/>`;
270
+ } else {
271
+ return `<${type}${attrs}>${innerHTML}</${type}>`;
272
+ }
187
273
  }
188
274
 
189
275
  // ---------- Trie-based router for dynamic routes ----------
@@ -397,7 +483,6 @@ export class Edge {
397
483
  const { ttl = 3600, staleWhileRevalidate = 0 } = options;
398
484
  const cache = caches.default;
399
485
  const responseClone = response.clone();
400
- // Modify the clone's headers directly instead of creating a new Response
401
486
  responseClone.headers.set('Cache-Control', `max-age=${ttl}${staleWhileRevalidate > 0 ? `, stale-while-revalidate=${staleWhileRevalidate}` : ''}`);
402
487
  responseClone.headers.delete('Set-Cookie');
403
488
  await cache.put(request, responseClone);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengkapp/edge",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Edge framework used by Lengkapp",
5
5
  "main": "edge-server.js",
6
6
  "types": "./edge-server.d.ts",
@@ -18,25 +18,13 @@
18
18
  "types": "./edge-server.d.ts",
19
19
  "default": "./edge-server.js"
20
20
  },
21
- "./client.min": {
22
- "types": "./client.min.d.ts",
23
- "default": "./edge-client.min.js"
24
- },
25
- "./server.min": {
26
- "types": "./server.min.d.ts",
27
- "default": "./edge-server.min.js"
28
- },
29
21
  "./package.json": "./package.json"
30
22
  },
31
23
  "files": [
32
24
  "edge-client.js",
33
- "edge-client.min.js",
34
- "edge-server.js",
35
- "edge-server.min.js",
36
- "edge-server.d.ts",
37
- "server.min.d.ts",
38
25
  "edge-client.d.ts",
39
- "client.min.d.ts"
26
+ "edge-server.js",
27
+ "edge-server.d.ts"
40
28
  ],
41
29
  "scripts": {
42
30
  "test": "echo \"No tests yet\""
package/client.min.d.ts DELETED
@@ -1,3 +0,0 @@
1
- // Type declarations for @lengkapp/edge/client.min
2
- // The client script is a side‑effect module with no exports.
3
- export {};
@@ -1 +0,0 @@
1
- (()=>{'use strict';const d=document,b=d.body,h=d.head,V=new Set(['click','dblclick','mousedown','mouseup','mouseover','mouseout','mouseenter','mouseleave','mousemove','contextmenu','focus','blur','focusin','focusout','keydown','keyup','keypress','change','input','submit','reset','load','DOMContentLoaded','ready','scroll','resize','wheel','touchstart','touchend','touchmove','visible','intersect']),T=new WeakMap,C=new WeakMap,R=new Set;if(!d.getElementById('s')){let s=d.createElement('style');s.id='s';s.textContent='.sk{display:flex;flex-direction:column;gap:8px;padding:10px}.sk .b{height:12px;background:linear-gradient(90deg,#e0e0e0 25%,#f0f0f0 50%,#e0e0e0 75%);background-size:200% 100%;animation:sh 1.5s infinite;border-radius:4px}@keyframes sh{0%{background-position:-200% 0}100%{background-position:200% 0}}.rt{display:inline-block;padding:8px 16px;background:#007bff;color:#fff;border-radius:4px;cursor:pointer;text-decoration:none;font-size:14px}.rt:hover{background:#0056b3}';h.appendChild(s)}let skT=d.createElement('template');skT.innerHTML='<div class="sk"><div class="b"></div><div class="b"></div><div class="b"></div></div>';let rtT=d.createElement('template');rtT.innerHTML='<span class="rt">Retry</span>';const phl=s=>{if(!s)return[];return s.replace(/^\[|\]$/g,'').split(',').map(x=>x.trim().replace(/^['"]|['"]$/g,'')).filter(Boolean)},inj=(r,t)=>{r.forEach(u=>{if(R.has(u))return;R.add(u);if(t==='css'){let l=d.createElement('link');l.rel='stylesheet';l.href=u;h.appendChild(l)}else if(t==='js'){let s=d.createElement('script');s.src=u;b.appendChild(s)}})},vis=el=>{let st=getComputedStyle(el);if(st.display==='none'||st.visibility==='hidden'||st.opacity==='0')return false;let r=el.getBoundingClientRect();return r.width>0&&r.height>0};let io=null;const gio=()=>{if(!io){io=new IntersectionObserver(es=>{es.forEach(e=>{let el=e.target,v=e.isIntersecting&&vis(el);if(v&&el.dataset.wasVisible!=='true')run(el);el.dataset.wasVisible=v?'true':'false'})},{threshold:0})}return io};const run=async el=>{if(el.dataset.running==='true')return;el.dataset.running='true';let post=el.hasAttribute('_post'),url=el.getAttribute('_post')||el.getAttribute('_get'),bd,hd={};if(post){let fid=el.getAttribute('_form'),js=el.getAttribute('_json');if(fid){let f=d.getElementById(fid);if(f)bd=new URLSearchParams(new FormData(f))}else if(js){bd=JSON.stringify(Object.fromEntries(js.split(',').map(n=>{let i=d.querySelector(`[name="${n}"]`);return[n,i?i.value:'']})));hd['Content-Type']='application/json'}}let ts=el.getAttribute('_target'),ct=C.get(el);if(!ct){ct=ts==='this'?el:d.querySelector(ts);if(ct)C.set(el,ct)}if(!ct){console.warn('Target not found:',ts);el.dataset.running='false';return}ct.replaceChildren(skT.content.cloneNode(true));try{let res=await fetch(url,{method:post?'POST':'GET',body:bd,headers:hd});let css=res.headers.get('x-css-required')||res.headers.get('x-css-requiered'),js=res.headers.get('x-js-required');if(css)inj(phl(css),'css');if(js)inj(phl(js),'js');ct.innerHTML=await res.text()}catch(e){console.error(e);let rt=rtT.content.firstElementChild.cloneNode(true);rt.addEventListener('click',ev=>{ev.preventDefault();ev.stopPropagation();run(el)});ct.replaceChildren(rt)}finally{el.dataset.running='false'}};const gte=el=>{let ev=T.get(el);if(ev)return ev;ev=new Set;let a=el.getAttribute('_trigger');if(a&&a.trim()){a.split(',').forEach(s=>{let e=s.trim().toLowerCase();if(V.has(e))ev.add(e)})}else{ev.add('click');if(el.getAttribute('_target')==='this')ev.add('load')}T.set(el,ev);return ev};const init=el=>{if(el.dataset.initialized==='true')return;el.dataset.initialized='true';let evs=gte(el);if(evs.has('load')||evs.has('domcontentloaded')||evs.has('ready'))run(el);if(evs.has('visible')||evs.has('intersect')){el.dataset.wasVisible='false';gio().observe(el)}};const delegated=['click','dblclick','mousedown','mouseup','mouseover','mouseout','mouseenter','mouseleave','mousemove','contextmenu','focus','blur','focusin','focusout','keydown','keyup','keypress','change','input','submit','reset','scroll','resize','wheel','touchstart','touchend','touchmove'];delegated.forEach(ev=>{let pass=['scroll','touchstart','touchmove','touchend','wheel'].includes(ev);d.addEventListener(ev,e=>{let t=e.target;if(!(t instanceof Element))return;let el=t.closest('[_get],[_post]');if(!el)return;let evs=gte(el);if(evs.has(ev)){if(ev==='click')e.preventDefault();run(el)}},pass?{passive:true}:false)});let mq=[],ms=false;const pm=()=>{ms=false;let nodes=mq;mq=[];nodes.forEach(n=>{if(n.nodeType!==1)return;if(n.matches('[_get],[_post]'))init(n);n.querySelectorAll('[_get],[_post]').forEach(init)})};new MutationObserver(ms=>{ms.forEach(m=>m.addedNodes.forEach(n=>{if(n.nodeType===1)mq.push(n)}));if(!ms){ms=true;Promise.resolve().then(pm)}}).observe(b,{childList:true,subtree:true});const initAll=()=>d.querySelectorAll('[_get],[_post]').forEach(init);if(d.readyState==='loading')d.addEventListener('DOMContentLoaded',initAll,{once:true});else initAll()})();
@@ -1,12 +0,0 @@
1
- class Context{constructor(r,e,x,p={},u=null){this.req=r;this.env=e;this.executionCtx=x;this.params=p;this.status=200;this.headers=new Headers;this._rawCookie=r.headers.get("Cookie")||"";this._cookies=null;this.url=u||new URL(r.url)}_ensureCookies(){if(this._cookies===null){let c={};if(this._rawCookie){for(const p of this._rawCookie.split(";")){const t=p.trim();if(!t)continue;const i=t.indexOf("=");if(i>0){const n=decodeURIComponent(t.slice(0,i)),v=decodeURIComponent(t.slice(i+1));c[n]=v}}}this._cookies=c}return this._cookies}getCookie(n){return this._ensureCookies()[n]??null}get query(){return this.url.searchParams}setCookie(n,v,o={}){let c=`${encodeURIComponent(n)}=${encodeURIComponent(v)}`;if(o.path)c+=`; Path=${o.path}`;if(o.domain)c+=`; Domain=${o.domain}`;if(o.maxAge!==undefined)c+=`; Max-Age=${o.maxAge}`;if(o.expires)c+=`; Expires=${o.expires.toUTCString()}`;if(o.secure)c+=`; Secure`;if(o.httpOnly)c+=`; HttpOnly`;if(o.sameSite)c+=`; SameSite=${o.sameSite}`;this.headers.append("Set-Cookie",c)}deleteCookie(n,o={}){this.setCookie(n,"",{...o,maxAge:0,expires:new Date(0)})}text(d,s=this.status,h={}){const r=this._buildHeaders(h);r.set("Content-Type","text/plain");return new Response(d,{status:s,headers:r})}json(d,s=this.status,h={}){const r=this._buildHeaders(h);r.set("Content-Type","application/json");return new Response(JSON.stringify(d),{status:s,headers:r})}html(d,s=this.status,h={}){const r=this._buildHeaders(h);r.set("Content-Type","text/html");return new Response(d,{status:s,headers:r})}_buildHeaders(h){if(Object.keys(h).length===0)return this.headers;const r=new Headers(this.headers);for(const k in h)if(Object.prototype.hasOwnProperty.call(h,k))r.set(k,h[k]);return r}}
2
- const Fragment=Symbol("Fragment");
3
- function jsx(t,p,...c){const n=p||{},f=c.flat(1/0);return{type:t,props:n,children:f,__isJSX:!0}}
4
- function renderToString(n){if(n==null||typeof n=="boolean")return"";if(typeof n=="string"||typeof n=="number")return escapeHtml(String(n));if(Array.isArray(n)){const a=[];for(let i=0;i<n.length;i++)a.push(renderToString(n[i]));return a.join("")}if(!n.__isJSX)return escapeHtml(String(n));const{type:t,props:p,children:c}=n;if(t===Fragment){const a=[];for(let i=0;i<c.length;i++)a.push(renderToString(c[i]));return a.join("")}if(typeof t=="symbol")return"";if(typeof t=="function")return renderToString(t({...p,children:c}));const ap=[];for(const k in p){if(k==="children")continue;const v=p[k];if(v==null||v===!1)continue;if(k==="className")ap.push(` class="${escapeHtml(v)}"`);else if(k==="htmlFor")ap.push(` for="${escapeHtml(v)}"`);else if(k.startsWith("on")&&typeof v=="function")continue;else if(k==="style"&&typeof v=="object"){const s=Object.entries(v).map(([a,b])=>`${camelToKebab(a)}:${b}`).join(";");ap.push(` style="${escapeHtml(s)}"`)}else if(v===!0)ap.push(` ${k}`);else ap.push(` ${k}="${escapeHtml(String(v))}"`)}const at=ap.join(""),cp=[];for(let i=0;i<c.length;i++)cp.push(renderToString(c[i]));return `<${t}${at}>${cp.join("")}</${t}>`}
5
- const HTML_ESCAPE_MAP={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"};
6
- function escapeHtml(s){return s.replace(/[&<>"']/g,c=>HTML_ESCAPE_MAP[c])}
7
- function camelToKebab(s){return s.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}
8
- class TrieNode{constructor(){this.children=new Map;this.paramChild=null;this.paramName=null;this.handler=null}}
9
- class RouteTrie{constructor(){this.root=new TrieNode}add(p,h,o){const s=p.split("/").filter(Boolean);let n=this.root;for(const seg of s){if(seg.startsWith(":")){if(!n.paramChild){n.paramChild=new TrieNode;n.paramName=seg.slice(1)}n=n.paramChild}else{if(!n.children.has(seg))n.children.set(seg,new TrieNode);n=n.children.get(seg)}}n.handler={handler:h,options:o}}match(p){const s=p.split("/").filter(Boolean);let n=this.root;const pr={};for(const seg of s){if(n.children.has(seg))n=n.children.get(seg);else if(n.paramChild){pr[n.paramName]=seg;n=n.paramChild}else return null}return n.handler?{handler:n.handler.handler,options:n.handler.options,params:pr}:null}}
10
- const HTTP_METHODS=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"];
11
- class Edge{constructor(){this.staticRoutes=new Map;for(const m of HTTP_METHODS)this.staticRoutes.set(m,new Map);this.dynamicTries={};for(const m of HTTP_METHODS)this.dynamicTries[m]=new RouteTrie;this.authKvBinding="AUTH_KV";this.rateLimitKvBinding="RATE_LIMIT_KV";this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}};this.scheduledHandler=null}_addRoute(m,p,o,h){if(typeof o=="boolean")o={auth:o};if(typeof o=="function"){h=o;o={}}if(typeof h!="function")throw new Error(`Handler for ${m} ${p} must be a function`);const mo={...this.defaults,...(o||{})};if(!p.includes(":"))this.staticRoutes.get(m).set(p,{handler:h,options:mo});else this.dynamicTries[m].add(p,h,mo)}get(p,o,h){this._addRoute("GET",p,o,h)}post(p,o,h){this._addRoute("POST",p,o,h)}put(p,o,h){this._addRoute("PUT",p,o,h)}delete(p,o,h){this._addRoute("DELETE",p,o,h)}patch(p,o,h){this._addRoute("PATCH",p,o,h)}options(p,o,h){this._addRoute("OPTIONS",p,o,h)}head(p,o,h){this._addRoute("HEAD",p,o,h)}scheduled(h){this.scheduledHandler=h}async _processAuth(c,f){if(!f)return!0;const t=c.getCookie("auth_token")||c.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!t)return!1;const kv=c.env[this.authKvBinding];if(!kv)return!1;const d=await kv.get(t);if(!d)return!1;if(typeof f=="object"){try{const pl=JSON.parse(d);if(f.role&&pl.role!==f.role)return!1;if(f.scopes){const us=pl.scopes||[];if(!f.scopes.every(s=>us.includes(s)))return!1}}catch{}}return!0}async _processRateLimit(c,f){if(!f)return!0;const o=f===!0?{}:f,{max=100,window=60}=o;const key=`rl:${c.req.headers.get("CF-Connecting-IP")||"unknown"}`;const kv=c.env[this.rateLimitKvBinding];if(!kv)return!0;let cnt=await kv.get(key,"json")||0;if(cnt>=max)return!1;cnt++;await kv.put(key,JSON.stringify(cnt),{expirationTtl:window});return!0}_processCors(c,f){if(!f)return;const o=typeof f=="object"?f:this.defaults.cors;c.headers.set("Access-Control-Allow-Origin",o.origin||"*");c.headers.set("Access-Control-Allow-Methods",o.methods||"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD");c.headers.set("Access-Control-Allow-Headers",o.headers||"Content-Type, Authorization");c.headers.set("Access-Control-Max-Age","86400")}_processLog(c,f){if(f)console.log(`${c.req.method} ${c.req.url} - ${c.status}`)}_processCompress(req,res){const ae=req.headers.get("Accept-Encoding")||"";let enc=null;if(ae.includes("gzip"))enc="gzip";else if(ae.includes("deflate"))enc="deflate";else if(ae.includes("br"))enc="br";if(!enc||!res.body)return res;const st=res.body.pipeThrough(new CompressionStream(enc));const nh=new Headers(res.headers);nh.set("Content-Encoding",enc);nh.set("Vary","Accept-Encoding");return new Response(st,{status:res.status,statusText:res.statusText,headers:nh})}async _validate(c,f){if(!f)return!0;if(typeof f=="function"){try{return!!(await f(c))}catch{return!1}}return!0}async _cacheGet(req){const cache=caches.default;return await cache.match(req)||null}async _cachePut(req,res,f){if(!f)return;const o=f===!0?{}:f,{ttl=3600,staleWhileRevalidate=0}=o;const cache=caches.default;const clone=res.clone();clone.headers.set("Cache-Control",`max-age=${ttl}${staleWhileRevalidate>0?`, stale-while-revalidate=${staleWhileRevalidate}`:""}`);clone.headers.delete("Set-Cookie");await cache.put(req,clone)}async fetch(req,env,ctx){const url=new URL(req.url),path=url.pathname,method=req.method;const mm=this.staticRoutes.get(method);if(mm){const sr=mm.get(path);if(sr)return this._handleRoute(sr.handler,sr.options,req,env,ctx,{},url)}const trie=this.dynamicTries[method];if(trie){const m=trie.match(path);if(m)return this._handleRoute(m.handler,m.options,req,env,ctx,m.params,url)}return new Response("Not Found",{status:404})}async _handleRoute(h,o,req,env,ctx,p,url){const c=new Context(req,env,ctx,p,url);if(!(await this._validate(c,o.validate)))return c.text("Validation failed",400);if(!(await this._processAuth(c,o.auth)))return c.text("Unauthorized",401);if(!(await this._processRateLimit(c,o.rateLimit)))return c.text("Too Many Requests",429);let cached=null;if(o.cache&&req.method==="GET"){cached=await this._cacheGet(req);if(cached){this._processCors(c,o.cors);this._processLog(c,o.log);return cached}}this._processCors(c,o.cors);let res;try{const r=await h(c);if(r&&r.__isJSX)res=c.html(renderToString(r));else res=r instanceof Response?r:c.text("OK");c.status=res.status}catch(e){console.error(e);res=c.text("Internal Server Error",500);c.status=500}if(o.cache&&req.method==="GET"&&res.status===200)c.executionCtx.waitUntil(this._cachePut(req,res.clone(),o.cache));if(o.compress)res=this._processCompress(req,res);this._processLog(c,o.log);return res}}
12
- export{Fragment,jsx,renderToString,Edge};
package/server.min.d.ts DELETED
@@ -1,83 +0,0 @@
1
- export const Fragment: unique symbol;
2
-
3
- export interface JSXNode {
4
- type: any;
5
- props: Record<string, any>;
6
- children: any[];
7
- __isJSX: true;
8
- }
9
-
10
- export function jsx(
11
- type: any,
12
- props?: Record<string, any> | null,
13
- ...children: any[]
14
- ): JSXNode;
15
-
16
- export function renderToString(node: any): string;
17
-
18
- export interface RouteOptions {
19
- auth?: boolean | { role?: string; scopes?: string[] };
20
- rateLimit?: boolean | { max?: number; window?: number };
21
- cors?: boolean | { origin?: string; methods?: string; headers?: string };
22
- validate?: (ctx: Context) => boolean | Promise<boolean>;
23
- log?: boolean;
24
- cache?: boolean | { ttl?: number; staleWhileRevalidate?: number };
25
- compress?: boolean;
26
- }
27
-
28
- export class Context {
29
- req: Request;
30
- env: any;
31
- executionCtx: ExecutionContext;
32
- params: Record<string, string>;
33
- status: number;
34
- headers: Headers;
35
-
36
- constructor(
37
- request: Request,
38
- env: any,
39
- executionCtx: ExecutionContext,
40
- params?: Record<string, string>,
41
- parsedUrl?: URL
42
- );
43
-
44
- getCookie(name: string): string | null;
45
- get query(): URLSearchParams;
46
- setCookie(name: string, value: string, options?: Record<string, any>): void;
47
- deleteCookie(name: string, options?: Record<string, any>): void;
48
- text(data: string, status?: number, headers?: Record<string, string>): Response;
49
- json(data: any, status?: number, headers?: Record<string, string>): Response;
50
- html(data: string, status?: number, headers?: Record<string, string>): Response;
51
- }
52
-
53
- export class Edge {
54
- constructor();
55
- authKvBinding: string;
56
- rateLimitKvBinding: string;
57
- defaults: {
58
- cors: { origin: string; methods: string };
59
- };
60
- scheduledHandler: ((...args: any[]) => void) | null;
61
-
62
- get(path: string, handler: (ctx: Context) => any): void;
63
- get(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
64
- post(path: string, handler: (ctx: Context) => any): void;
65
- post(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
66
- put(path: string, handler: (ctx: Context) => any): void;
67
- put(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
68
- delete(path: string, handler: (ctx: Context) => any): void;
69
- delete(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
70
- patch(path: string, handler: (ctx: Context) => any): void;
71
- patch(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
72
- options(path: string, handler: (ctx: Context) => any): void;
73
- options(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
74
- head(path: string, handler: (ctx: Context) => any): void;
75
- head(path: string, options: RouteOptions, handler: (ctx: Context) => any): void;
76
- scheduled(handler: (...args: any[]) => void): void;
77
-
78
- fetch(
79
- request: Request,
80
- env: any,
81
- executionCtx: ExecutionContext
82
- ): Promise<Response>;
83
- }