@lengkapp/edge 0.0.3 → 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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ LengkApp Edge License
2
+
3
+ Copyright (c) LengkApp — Yasir Haris
4
+ Contact: yh@lengk.app / yasir.haris@gmail.com
5
+
6
+ Permission is granted to any person obtaining a copy of this software
7
+ and associated documentation files (the "Software") to use, copy, and
8
+ distribute the Software free of charge, for any purpose, including
9
+ commercial use, subject to the following conditions:
10
+
11
+ 1. The Software may not be modified, adapted, or altered in any way
12
+ without prior written permission from the copyright holder.
13
+
14
+ 2. Redistribution of the Software, in whole or in part, must retain
15
+ this LICENSE file unmodified and include the copyright notice above.
16
+
17
+ 3. This permission notice does not grant any right to use the
18
+ LengkApp name, brand, or trademarks without separate written
19
+ permission.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE
25
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # @lengkapp/edge
2
+
3
+ A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
4
+
5
+ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**. See [Performance](#performance) for a local benchmark against Hono, `hono/tiny`, and a plain native Workers handler.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Trie-based routing** – static & dynamic routes (`/users/:id`) with fast lookups.
12
+ - **JSX support** – render JSX components to HTML without a build step (using `jsx` and `renderToString`).
13
+ - **Middleware options** – authentication, rate limiting, CORS, logging, caching, compression, and custom validation.
14
+ - **Cookie helpers** – set, get, and delete cookies with flexible options.
15
+ - **Declarative client** – enhance HTML with `_get`, `_post`, `_target`, `_trigger` attributes to fetch and replace content dynamically, with skeleton loaders and retry buttons.
16
+ - **Resource injection** – automatically load CSS/JS files from response headers.
17
+ - **Scheduled tasks** – built-in support for Cron triggers.
18
+ - **Zero dependencies** – lightweight and fast.
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @lengkapp/edge
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ **1. Create a worker script (`worker.js` or `src/index.ts`):**
33
+
34
+ ```ts
35
+ import { Edge, renderToString } from '@lengkapp/edge';
36
+
37
+ const app = new Edge();
38
+
39
+ app.get('/', (ctx) => ctx.text('Hello World!'));
40
+
41
+ app.get('/users/:id', (ctx) => {
42
+ return ctx.json({ id: ctx.params.id });
43
+ });
44
+
45
+ export default {
46
+ fetch: (request, env, ctx) => app.fetch(request, env, ctx),
47
+ };
48
+ ```
49
+
50
+ **2. Create `wrangler.jsonc`:**
51
+
52
+ Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
53
+
54
+ ```jsonc
55
+ {
56
+ "$schema": "./node_modules/wrangler/config-schema.json",
57
+ "name": "my-edge-app",
58
+ "main": "worker.js",
59
+ // Set this to today's date
60
+ "compatibility_date": "2026-09-06",
61
+ "observability": {
62
+ "enabled": true
63
+ }
64
+ }
65
+ ```
66
+
67
+ **3. Deploy:**
68
+
69
+ ```bash
70
+ wrangler deploy
71
+ ```
72
+
73
+ > `wrangler publish` was removed in favor of `wrangler deploy`. If you're on an older Wrangler version, run `npm install -g wrangler@latest` first.
74
+
75
+ ---
76
+
77
+ ## Server API
78
+
79
+ ### Creating an App
80
+
81
+ ```ts
82
+ import { Edge } from '@lengkapp/edge';
83
+ const app = new Edge();
84
+ ```
85
+
86
+ ### Routing
87
+
88
+ Register routes using HTTP method helpers. Both static and dynamic paths are supported.
89
+
90
+ ```ts
91
+ // Static
92
+ app.get('/home', handler);
93
+ app.post('/submit', handler);
94
+
95
+ // Dynamic (colon-prefixed segments)
96
+ app.get('/users/:id', (ctx) => {
97
+ const userId = ctx.params.id;
98
+ });
99
+ ```
100
+
101
+ Supported methods: `get`, `post`, `put`, `delete`, `patch`, `options`, `head`.
102
+
103
+ ### Context Object
104
+
105
+ Each handler receives a `Context` object (`ctx`) with the following members:
106
+
107
+ | Property / Method | Description |
108
+ | --- | --- |
109
+ | `ctx.req` | The original `Request` object |
110
+ | `ctx.env` | The environment bindings (KV, secrets, etc.) |
111
+ | `ctx.executionCtx` | The `ExecutionContext` (for `waitUntil`) |
112
+ | `ctx.params` | Route parameters (e.g., `{ id: '123' }`) |
113
+ | `ctx.status` | HTTP status code (default `200`) |
114
+ | `ctx.headers` | `Headers` object for the response |
115
+ | `ctx.query` | `URLSearchParams` (lazy) |
116
+ | `ctx.getCookie(name)` | Get a cookie value |
117
+ | `ctx.setCookie(name, value, options)` | Set a cookie |
118
+ | `ctx.deleteCookie(name, options)` | Delete a cookie (expires immediately) |
119
+ | `ctx.text(data, status?, headers?)` | Return plain text |
120
+ | `ctx.json(data, status?, headers?)` | Return JSON |
121
+ | `ctx.html(data, status?, headers?)` | Return HTML |
122
+
123
+ Cookie options: `path`, `domain`, `maxAge`, `expires`, `secure`, `httpOnly`, `sameSite`.
124
+
125
+ ### Returning Responses
126
+
127
+ You can return a `Response` object, a string (becomes text), or an object (automatically JSON).
128
+ If the handler returns a JSX element, it will be rendered to HTML automatically (see [JSX Support](#jsx-support)).
129
+
130
+ ```ts
131
+ app.get('/json', (ctx) => {
132
+ return { hello: 'world' }; // → ctx.json()
133
+ });
134
+
135
+ app.get('/html', (ctx) => {
136
+ return '<h1>Hi</h1>'; // → ctx.html()
137
+ });
138
+ ```
139
+
140
+ ### Route Options
141
+
142
+ Pass an options object as the second argument (before the handler) to enable middleware.
143
+
144
+ #### Authentication (`auth`)
145
+
146
+ Requires an `AUTH_KV` KV binding. Valid tokens are stored as keys with a JSON payload.
147
+
148
+ ```ts
149
+ app.get('/protected', { auth: true }, handler);
150
+ app.get('/admin', { auth: { role: 'admin' } }, handler);
151
+ app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
152
+ ```
153
+
154
+ The client must send the token via `Authorization: Bearer <token>` or a cookie named `auth_token`.
155
+
156
+ #### Rate Limiting (`rateLimit`)
157
+
158
+ Requires a `RATE_LIMIT_KV` binding.
159
+
160
+ ```ts
161
+ app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
162
+ ```
163
+
164
+ #### Caching (`cache`)
165
+
166
+ Caches successful GET responses in the Cloudflare cache.
167
+
168
+ ```ts
169
+ app.get('/cached', { cache: { ttl: 60, staleWhileRevalidate: 30 } }, handler);
170
+ ```
171
+
172
+ #### CORS (`cors`)
173
+
174
+ Enables CORS headers.
175
+
176
+ ```ts
177
+ app.get('/api', { cors: true }, handler);
178
+
179
+ // Custom origin / methods
180
+ app.get('/api', { cors: { origin: 'https://example.com', methods: 'GET,POST' } }, handler);
181
+ ```
182
+
183
+ #### Logging (`log`)
184
+
185
+ Logs method, URL, and status to the console.
186
+
187
+ #### Compression (`compress`)
188
+
189
+ Compresses the response using gzip/deflate/br if the client supports it.
190
+
191
+ #### Custom Validation (`validate`)
192
+
193
+ A function returning `true`/`false` (or a `Promise`) to allow/deny the request.
194
+
195
+ ```ts
196
+ app.post('/submit', {
197
+ validate: (ctx) => ctx.req.headers.get('Authorization') === 'Bearer secret'
198
+ }, handler);
199
+ ```
200
+
201
+ ---
202
+
203
+ ## JSX Support
204
+
205
+ The package includes a minimal JSX runtime. Write components as functions returning JSX.
206
+
207
+ ```tsx
208
+ import { jsx, renderToString, Fragment } from '@lengkapp/edge';
209
+
210
+ function Card({ title }) {
211
+ return (
212
+ <div class="card">
213
+ <h2>{title}</h2>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ app.get('/card', (ctx) => {
219
+ return ctx.html(renderToString(<Card title="Hello" />));
220
+ });
221
+ ```
222
+
223
+ If your handler returns a JSX element directly, Edge will automatically render it:
224
+
225
+ ```tsx
226
+ app.get('/auto', (ctx) => <h1>Auto rendered</h1>);
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Scheduled Tasks
232
+
233
+ Register a scheduled handler for Cron triggers.
234
+
235
+ ```ts
236
+ app.scheduled(async (event, env, ctx) => {
237
+ console.log('Cron executed:', event.cron);
238
+ });
239
+ ```
240
+
241
+ Then export it in your worker:
242
+
243
+ ```ts
244
+ export default {
245
+ fetch: (req, env, ctx) => app.fetch(req, env, ctx),
246
+ scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
247
+ };
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Client (Declarative Partial Updates)
253
+
254
+ Add the client script to your page (or include it via a CDN):
255
+
256
+ ```html
257
+ <script src="https://cdn.example.com/edge-client.min.js"></script>
258
+ ```
259
+
260
+ Then use HTML attributes to make elements fetch content asynchronously.
261
+
262
+ ### Attributes
263
+
264
+ | Attribute | Description |
265
+ | --- | --- |
266
+ | `_get` | URL to fetch via GET |
267
+ | `_post` | URL to fetch via POST |
268
+ | `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. |
269
+ | `_trigger` | Comma-separated list of events that trigger the fetch (default: `click`; `load` if `_target="this"`). Supported: `click`, `load`, `visible`, `intersect`, `submit`, etc. |
270
+ | `_form` | (with `_post`) ID of a form to serialize as the POST body (URL-encoded). |
271
+ | `_json` | (with `_post`) Comma-separated names of input fields to send as JSON. |
272
+
273
+ ### Examples
274
+
275
+ **Load content on click:**
276
+
277
+ ```html
278
+ <button _get="/more-posts" _target="#posts" _trigger="click">Load More</button>
279
+ <div id="posts"></div>
280
+ ```
281
+
282
+ **Auto-load on page load:**
283
+
284
+ ```html
285
+ <div _get="/user-profile" _target="this"></div>
286
+ ```
287
+
288
+ **Post a form via AJAX:**
289
+
290
+ ```html
291
+ <form id="contact-form">
292
+ <input name="email" />
293
+ <button _post="/submit" _form="contact-form" _target="#result">Submit</button>
294
+ </form>
295
+ <div id="result"></div>
296
+ ```
297
+
298
+ **Send JSON data:**
299
+
300
+ ```html
301
+ <input name="username" />
302
+ <input name="password" />
303
+ <button _post="/login" _json="username,password" _target="#status">Login</button>
304
+ ```
305
+
306
+ **Load when element becomes visible (IntersectionObserver):**
307
+
308
+ ```html
309
+ <div _get="/lazy-content" _target="this" _trigger="visible"></div>
310
+ ```
311
+
312
+ ### Skeleton Loading
313
+
314
+ While the request is in progress, the target container is filled with a skeleton loader (three shimmer bars). On error, a "Retry" button is shown.
315
+
316
+ ### Resource Injection
317
+
318
+ If the server response includes headers `x-css-required` or `x-js-required`, the client will automatically inject those resources (once per URL) into the page.
319
+
320
+ Example server route:
321
+
322
+ ```ts
323
+ app.get('/widget', (ctx) => {
324
+ ctx.headers.set('x-css-required', '["/widget.css"]');
325
+ ctx.headers.set('x-js-required', '["/widget.js"]');
326
+ return ctx.html('<div class="widget">...</div>');
327
+ });
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Configuration
333
+
334
+ ### `wrangler.jsonc`
335
+
336
+ If you use authentication or rate limiting, you need KV namespaces. Create them first:
337
+
338
+ ```bash
339
+ wrangler kv namespace create AUTH_KV
340
+ wrangler kv namespace create RATE_LIMIT_KV
341
+ ```
342
+
343
+ Then wire up the returned IDs in your config:
344
+
345
+ ```jsonc
346
+ {
347
+ "$schema": "./node_modules/wrangler/config-schema.json",
348
+ "name": "my-edge-app",
349
+ "main": "worker.js",
350
+ // Set this to today's date
351
+ "compatibility_date": "2026-09-06",
352
+ "observability": {
353
+ "enabled": true
354
+ },
355
+ "kv_namespaces": [
356
+ { "binding": "AUTH_KV", "id": "your-auth-kv-id" },
357
+ { "binding": "RATE_LIMIT_KV", "id": "your-ratelimit-kv-id" }
358
+ ],
359
+ // Optional Cron triggers
360
+ "triggers": {
361
+ "crons": ["*/5 * * * *"]
362
+ }
363
+ }
364
+ ```
365
+
366
+ <details>
367
+ <summary>Equivalent <code>wrangler.toml</code></summary>
368
+
369
+ ```toml
370
+ name = "my-edge-app"
371
+ main = "worker.js"
372
+ compatibility_date = "2026-09-06"
373
+
374
+ [observability]
375
+ enabled = true
376
+
377
+ [[kv_namespaces]]
378
+ binding = "AUTH_KV"
379
+ id = "your-auth-kv-id"
380
+
381
+ [[kv_namespaces]]
382
+ binding = "RATE_LIMIT_KV"
383
+ id = "your-ratelimit-kv-id"
384
+
385
+ # Optional Cron triggers
386
+ [triggers]
387
+ crons = ["*/5 * * * *"]
388
+ ```
389
+
390
+ </details>
391
+
392
+ Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
393
+
394
+ ### Environment Variables
395
+
396
+ The KV binding names are configurable on the Edge instance:
397
+
398
+ ```ts
399
+ app.authKvBinding = 'CUSTOM_AUTH_KV';
400
+ app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Full Example
406
+
407
+ ```ts
408
+ import { Edge, renderToString } from '@lengkapp/edge';
409
+ import { HomePage } from './Page.jsx';
410
+
411
+ const app = new Edge();
412
+
413
+ // Basic text
414
+ app.get('/', (ctx) => ctx.text('Hello from Edge!'));
415
+
416
+ // JSX component
417
+ app.get('/home', (ctx) => ctx.html(renderToString(<HomePage />)));
418
+
419
+ // JSON with route params
420
+ app.get('/users/:id', (ctx) => ctx.json({ userId: ctx.params.id }));
421
+
422
+ // POST JSON
423
+ app.post('/users', async (ctx) => {
424
+ const body = await ctx.req.json();
425
+ return ctx.json({ created: true, user: body }, 201);
426
+ });
427
+
428
+ // Auth
429
+ app.get('/protected', { auth: true }, (ctx) => ctx.text('Authenticated'));
430
+
431
+ // Rate limiting
432
+ app.get('/limited', { rateLimit: { max: 5, window: 60 } }, (ctx) => ctx.text('Limited'));
433
+
434
+ // Caching
435
+ app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
436
+
437
+ // Cookies
438
+ app.get('/set-cookie', (ctx) => {
439
+ ctx.setCookie('session', 'abc123', { httpOnly: true, path: '/' });
440
+ return ctx.text('Cookie set');
441
+ });
442
+
443
+ app.get('/get-cookie', (ctx) => {
444
+ const session = ctx.getCookie('session');
445
+ return ctx.text(`Cookie: ${session}`);
446
+ });
447
+
448
+ // Scheduled
449
+ app.scheduled(async (event, env, ctx) => {
450
+ console.log('Cron:', event.cron);
451
+ });
452
+
453
+ export default {
454
+ fetch: (req, env, ctx) => app.fetch(req, env, ctx),
455
+ scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
456
+ };
457
+ ```
458
+
459
+ ---
460
+
461
+ ## Performance
462
+
463
+ @lengkapp/edge is inspired by [Hono](https://hono.dev) — one of the fastest Workers frameworks around — but ships with **zero dependencies**. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
464
+
465
+ ### Test setup
466
+
467
+ - **Tool:** `autocannon` — 10 connections, 10s run per route
468
+ - **Target:** local `wrangler dev` server
469
+ - **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
470
+ - **Contenders:** `@lengkapp/edge`, `hono`, `hono/tiny`, and a native Workers handler with no framework
471
+
472
+ > This is a single local run, not a formal benchmark suite — treat the numbers as directional. Latency stdev was ±3–4 ms across all four, so differences smaller than that are within noise.
473
+
474
+ ### Results
475
+
476
+ | Framework | Route | Avg Req/sec | Avg Latency | Requests | Data Read |
477
+ | --- | --- | --- | --- | --- | --- |
478
+ | **@lengkapp/edge** | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
479
+ | **@lengkapp/edge** | `/json` | **500.4** | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
480
+ | Hono | `/text` | **506.0** | 19.27 ms | 5,000 / 10.03s | 455 kB |
481
+ | Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
482
+ | Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
483
+ | Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
484
+ | Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
485
+ | Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
486
+
487
+ ```mermaid
488
+ xychart-beta
489
+ title "Avg requests/sec — GET /text"
490
+ x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
491
+ y-axis "Req/sec" 490 --> 510
492
+ bar [503.1, 506.0, 505.5, 498.4]
493
+ ```
494
+
495
+ ```mermaid
496
+ xychart-beta
497
+ title "Avg requests/sec — GET /json"
498
+ x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
499
+ y-axis "Req/sec" 490 --> 505
500
+ bar [500.4, 495.2, 497.9, 499.7]
501
+ ```
502
+
503
+ ### Takeaways
504
+
505
+ - On `/json`, @lengkapp/edge posted the highest average throughput of the four, ahead of both Hono builds and the native handler.
506
+ - On `/text`, it landed within ~1% of `hono`/`hono/tiny` and ahead of the native baseline — effectively a tie once you account for run-to-run variance.
507
+ - All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with **zero runtime dependencies**, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
508
+ - @lengkapp/edge read more bytes per request than the other three in this run — worth profiling if minimal payload size matters for your use case.
509
+
510
+ ---
511
+
512
+ ## License
513
+
514
+ See [LICENSE](./LICENSE). © LengkApp — Yasir Haris