lacis 0.2.0
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 +21 -0
- package/README.md +395 -0
- package/dist/adapters/index.d.ts +14 -0
- package/dist/adapters/index.js +1 -0
- package/dist/chunk-NVNSYLVY.js +2075 -0
- package/dist/cli/index.js +158 -0
- package/dist/index-rE4kFMlu.d.ts +192 -0
- package/dist/index.d.ts +343 -0
- package/dist/index.js +457 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gradle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Lacis
|
|
2
|
+
|
|
3
|
+
Zero-dependency TypeScript web framework with file-based routing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **File-based routing** — routes generated automatically from your `routes/` folder
|
|
8
|
+
- **Standard Schema validation** — validate params, query, and body with Zod, Valibot, or ArkType via `defineHandler`
|
|
9
|
+
- **OpenAPI generation** — spec built automatically from your `defineHandler` routes
|
|
10
|
+
- **Middleware** — global, path-scoped, and route-scoped via `+middleware.ts` files
|
|
11
|
+
- **CORS & rate limiting** — built in, zero dependencies
|
|
12
|
+
- **SSE** — server-sent events with a matching client helper
|
|
13
|
+
- **Multi-platform** — Node.js, Bun, Vercel, Netlify via adapters
|
|
14
|
+
- **Cookies** — first-class `req.cookies` / `res.cookies` API
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install lacis
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CLI
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lacis dev # start dev server (auto-detects platform)
|
|
26
|
+
lacis build # generate routes/_manifest.ts
|
|
27
|
+
lacis watch # watch routes and regenerate manifest on changes
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
All commands accept `--routes <dir>` to override the default `./routes` directory.
|
|
31
|
+
|
|
32
|
+
## Project structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-app/
|
|
36
|
+
routes/
|
|
37
|
+
+middleware.ts # global middleware
|
|
38
|
+
index.ts # GET /
|
|
39
|
+
users/
|
|
40
|
+
index.ts # GET /users, POST /users
|
|
41
|
+
[id]/
|
|
42
|
+
index.ts # GET /users/:id
|
|
43
|
+
api/
|
|
44
|
+
+middleware.ts # middleware scoped to /api/*
|
|
45
|
+
items/
|
|
46
|
+
index.ts
|
|
47
|
+
server.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Routing
|
|
51
|
+
|
|
52
|
+
Each file in `routes/` exports named HTTP method handlers or a default export.
|
|
53
|
+
|
|
54
|
+
**Named exports**
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// routes/users/index.ts
|
|
58
|
+
import type { Request, Response } from 'lacis'
|
|
59
|
+
|
|
60
|
+
export async function GET(req: Request, res: Response) {
|
|
61
|
+
res.status(200).json({ users: [] })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function POST(req: Request, res: Response) {
|
|
65
|
+
const body = await req.json()
|
|
66
|
+
res.status(201).json({ created: body })
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Default export**
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export default async function handler(req: Request, res: Response) {
|
|
74
|
+
res.json({ method: req.method })
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Dynamic routes**
|
|
79
|
+
|
|
80
|
+
Use bracket syntax for URL parameters: `routes/users/[id]/index.ts` → `/users/:id`
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
export async function GET(req: Request, res: Response) {
|
|
84
|
+
const { id } = req.params!
|
|
85
|
+
res.json({ id })
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Request / Response API
|
|
90
|
+
|
|
91
|
+
**Request**
|
|
92
|
+
|
|
93
|
+
| Property / Method | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `req.params` | URL path parameters |
|
|
96
|
+
| `req.query` | Parsed query string |
|
|
97
|
+
| `req.cookies.get(name)` | Read a cookie |
|
|
98
|
+
| `req.cookies.all()` | All cookies as an object |
|
|
99
|
+
| `req.json<T>()` | Parse JSON body |
|
|
100
|
+
| `req.form<T>()` | Parse form body |
|
|
101
|
+
| `req.body()` | Raw body as `Buffer` |
|
|
102
|
+
| `req.getHeader(name)` | Read a request header |
|
|
103
|
+
|
|
104
|
+
**Response**
|
|
105
|
+
|
|
106
|
+
| Method | Description |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `res.status(code)` | Set status code (chainable) |
|
|
109
|
+
| `res.json(data)` | Send JSON response |
|
|
110
|
+
| `res.send(data)` | Send string or JSON |
|
|
111
|
+
| `res.setHeader(name, value)` | Set a response header |
|
|
112
|
+
| `res.cookies.set(name, value, opts?)` | Set a cookie |
|
|
113
|
+
| `res.cookies.delete(name, opts?)` | Delete a cookie |
|
|
114
|
+
|
|
115
|
+
## defineHandler
|
|
116
|
+
|
|
117
|
+
`defineHandler` wraps a route handler to add validation and OpenAPI metadata. It supports any library that implements the [Standard Schema](https://standardschema.dev/) spec: Zod 3.24+, Valibot, ArkType.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// routes/users/[id]/index.ts
|
|
121
|
+
import { defineHandler } from 'lacis'
|
|
122
|
+
import { z } from 'zod'
|
|
123
|
+
|
|
124
|
+
export const GET = defineHandler({
|
|
125
|
+
params: z.object({ id: z.string() }),
|
|
126
|
+
query: z.object({ verbose: z.boolean().optional() }),
|
|
127
|
+
meta: { summary: 'Get user by ID', tags: ['users'] },
|
|
128
|
+
handler: async (req, res) => {
|
|
129
|
+
req.params.id // string — typed and validated
|
|
130
|
+
req.query.verbose // boolean | undefined — typed and validated
|
|
131
|
+
res.json({ id: req.params.id })
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
export const POST = defineHandler({
|
|
136
|
+
body: z.object({ name: z.string(), email: z.string().email() }),
|
|
137
|
+
meta: { summary: 'Create user', tags: ['users'] },
|
|
138
|
+
handler: async (req, res) => {
|
|
139
|
+
const { name, email } = req.body // typed
|
|
140
|
+
res.status(201).json({ name, email })
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Validation failures return a `400` automatically:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"error": "Validation failed",
|
|
150
|
+
"issues": [{ "message": "Required", "path": ["email"] }]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**With Valibot**
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import * as v from 'valibot'
|
|
158
|
+
|
|
159
|
+
export const GET = defineHandler({
|
|
160
|
+
params: v.object({ id: v.string() }),
|
|
161
|
+
handler: async (req, res) => { ... },
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**With ArkType**
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { type } from 'arktype'
|
|
169
|
+
|
|
170
|
+
export const GET = defineHandler({
|
|
171
|
+
query: type({ 'page?': 'number' }),
|
|
172
|
+
handler: async (req, res) => { ... },
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## OpenAPI
|
|
177
|
+
|
|
178
|
+
Add `openapi` to your server config to expose a generated spec at runtime:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
createServer(routesDir, {
|
|
182
|
+
openapi: {
|
|
183
|
+
path: '/openapi.json', // default
|
|
184
|
+
info: { title: 'My API', version: '1.0.0' },
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The spec is built from all `defineHandler` routes. Routes without `defineHandler` appear with a generic `200` response. Converters required per library:
|
|
190
|
+
|
|
191
|
+
| Library | Package to install |
|
|
192
|
+
|---|---|
|
|
193
|
+
| Zod | `zod-to-json-schema` |
|
|
194
|
+
| Valibot | `@valibot/to-json-schema` |
|
|
195
|
+
| ArkType | none (native `.toJsonSchema()`) |
|
|
196
|
+
|
|
197
|
+
## Middleware
|
|
198
|
+
|
|
199
|
+
Create a `+middleware.ts` file in any route directory. It applies to all routes at and below that path.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// routes/api/+middleware.ts
|
|
203
|
+
import type { Request, Response } from 'lacis'
|
|
204
|
+
|
|
205
|
+
export const beforeRequest = async (req: Request, res: Response) => {
|
|
206
|
+
if (!req.getHeader('authorization')) {
|
|
207
|
+
res.status(401).json({ error: 'Unauthorized' })
|
|
208
|
+
return false // stops the request
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const afterRequest = async (req: Request, res: Response) => {
|
|
213
|
+
// runs after the handler
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const onError = async (req: Request, res: Response, context: any) => {
|
|
217
|
+
console.error(context.error)
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Returning `false` from `beforeRequest` stops the request pipeline.
|
|
222
|
+
|
|
223
|
+
**Global middleware via server config**
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
createServer(routesDir, {
|
|
227
|
+
middleware: {
|
|
228
|
+
beforeRequest: async (req, res) => { /* ... */ },
|
|
229
|
+
afterRequest: async (req, res) => { /* ... */ },
|
|
230
|
+
onError: async (req, res, ctx) => { /* ... */ },
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## CORS
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
createServer(routesDir, {
|
|
239
|
+
cors: {
|
|
240
|
+
origin: 'https://myapp.com', // string, string[], RegExp, or (origin) => boolean
|
|
241
|
+
credentials: true,
|
|
242
|
+
methods: ['GET', 'POST'], // default: all methods
|
|
243
|
+
allowedHeaders: ['Authorization', 'Content-Type'],
|
|
244
|
+
exposedHeaders: ['X-Total-Count'],
|
|
245
|
+
maxAge: 86400,
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`origin: '*'` is incompatible with `credentials: true` — Lacis reflects the actual origin automatically in that case.
|
|
251
|
+
|
|
252
|
+
You can also create a standalone middleware:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { createCorsMiddleware } from 'lacis'
|
|
256
|
+
|
|
257
|
+
const cors = createCorsMiddleware({ origin: '*' })
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Rate limiting
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { createRateLimit } from 'lacis'
|
|
264
|
+
|
|
265
|
+
createServer(routesDir, {
|
|
266
|
+
middleware: {
|
|
267
|
+
beforeRequest: createRateLimit({
|
|
268
|
+
windowMs: 60_000, // 1 minute
|
|
269
|
+
max: 100,
|
|
270
|
+
message: 'Too Many Requests',
|
|
271
|
+
keyGenerator: (req) => req.getHeader('x-forwarded-for') ?? 'unknown',
|
|
272
|
+
}),
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers on every response. Returns `429` with `Retry-After` when the limit is exceeded.
|
|
278
|
+
|
|
279
|
+
## Server-Sent Events
|
|
280
|
+
|
|
281
|
+
**Server**
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// routes/stream/index.ts
|
|
285
|
+
import type { Request, Response } from 'lacis'
|
|
286
|
+
|
|
287
|
+
export async function GET(req: Request, res: Response) {
|
|
288
|
+
res.initSSE()
|
|
289
|
+
|
|
290
|
+
res.sseJson({ status: 'connected' })
|
|
291
|
+
res.sseEvent('update', { id: 1, value: 42 })
|
|
292
|
+
res.sseClose()
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Method | Description |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `res.initSSE(options?)` | Initialize SSE response |
|
|
299
|
+
| `res.sseSend(data)` | Send raw string data |
|
|
300
|
+
| `res.sseJson(data)` | Send JSON data |
|
|
301
|
+
| `res.sseEvent(event, data)` | Send named event with JSON data |
|
|
302
|
+
| `res.sseComment(comment)` | Send a comment (keepalive) |
|
|
303
|
+
| `res.sseId(id)` | Set event ID |
|
|
304
|
+
| `res.sseRetry(ms)` | Set client retry interval |
|
|
305
|
+
| `res.sseClose(comment?)` | Close the connection |
|
|
306
|
+
|
|
307
|
+
**Client**
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { createSSEClient } from 'lacis'
|
|
311
|
+
|
|
312
|
+
const client = await createSSEClient('http://localhost:3000/stream')
|
|
313
|
+
|
|
314
|
+
client
|
|
315
|
+
.onMessage(data => console.log('message:', data))
|
|
316
|
+
.onEvent('update', data => console.log('update:', data))
|
|
317
|
+
.onClose(() => console.log('closed'))
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`createSSEClient` options:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
createSSEClient(url, {
|
|
324
|
+
method: 'GET', // default GET, POST if body is provided
|
|
325
|
+
body: { token: 'abc' }, // sent as JSON if provided
|
|
326
|
+
reconnectInterval: 3000,
|
|
327
|
+
maxRetries: 3,
|
|
328
|
+
disableReconnect: false,
|
|
329
|
+
params: { key: 'value' }, // appended to URL query string
|
|
330
|
+
})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Server configuration
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { createServer } from 'lacis'
|
|
337
|
+
|
|
338
|
+
createServer(routesDir, {
|
|
339
|
+
port: 3000,
|
|
340
|
+
isDev: process.env.NODE_ENV === 'development',
|
|
341
|
+
platform: 'node', // 'node' | 'bun' | 'vercel' | 'netlify'
|
|
342
|
+
timeout: 30000,
|
|
343
|
+
|
|
344
|
+
defaultHeaders: {
|
|
345
|
+
'X-Powered-By': 'Lacis',
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
httpsOptions: {
|
|
349
|
+
cert: fs.readFileSync('cert.pem'),
|
|
350
|
+
key: fs.readFileSync('key.pem'),
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
cluster: {
|
|
354
|
+
enabled: true,
|
|
355
|
+
workers: 4, // defaults to CPU count
|
|
356
|
+
// Node: fork-based cluster, OS round-robin scheduling
|
|
357
|
+
// Bun: Bun.spawn() workers with reusePort
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// Dev only — exposes /health endpoint with request metrics
|
|
361
|
+
monitoring: {
|
|
362
|
+
enabled: true,
|
|
363
|
+
sampleInterval: 5000,
|
|
364
|
+
reportInterval: 60000,
|
|
365
|
+
thresholds: {
|
|
366
|
+
cpu: 80,
|
|
367
|
+
memory: 80,
|
|
368
|
+
responseTime: 1000,
|
|
369
|
+
errorRate: 5,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Adapters
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { createServer, getRoutesDir } from 'lacis'
|
|
379
|
+
|
|
380
|
+
// Node.js
|
|
381
|
+
createServer(getRoutesDir(), { platform: 'node' })
|
|
382
|
+
|
|
383
|
+
// Bun
|
|
384
|
+
createServer(getRoutesDir(), { platform: 'bun' })
|
|
385
|
+
|
|
386
|
+
// Vercel
|
|
387
|
+
export default createServer(getRoutesDir(), { platform: 'vercel' })
|
|
388
|
+
|
|
389
|
+
// Netlify
|
|
390
|
+
export const handler = createServer(getRoutesDir(), { platform: 'netlify' })
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { i as Adapter } from '../index-rE4kFMlu.js';
|
|
2
|
+
import 'http';
|
|
3
|
+
|
|
4
|
+
declare const nodeAdapter: Adapter;
|
|
5
|
+
|
|
6
|
+
declare const vercelAdapter: Adapter;
|
|
7
|
+
|
|
8
|
+
declare const netlifyAdapter: Adapter;
|
|
9
|
+
|
|
10
|
+
declare const bunAdapter: Adapter;
|
|
11
|
+
|
|
12
|
+
declare function getAdapter(platform?: string): Adapter;
|
|
13
|
+
|
|
14
|
+
export { bunAdapter, getAdapter, netlifyAdapter, nodeAdapter, vercelAdapter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { bunAdapter, getAdapter, netlifyAdapter, nodeAdapter, vercelAdapter } from '../chunk-NVNSYLVY.js';
|