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 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';