@rip-lang/server 1.2.11 → 1.3.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/README.md CHANGED
@@ -2,32 +2,34 @@
2
2
 
3
3
  # Rip Server - @rip-lang/server
4
4
 
5
- > **A production-grade application server with multi-worker processes, hot reload, HTTPS, and mDNS — written entirely in Rip**
5
+ > **A full-stack web framework and production server routing, middleware, multi-worker processes, hot reload, HTTPS, and mDNS — written entirely in Rip**
6
6
 
7
- Rip Server is a self-contained application server that turns any
8
- [@rip-lang/api](https://github.com/shreeve/rip-lang/tree/main/packages/api)
9
- app into a production-ready service. It handles multi-worker process management,
7
+ Rip Server is a unified web framework and application server. It provides
8
+ Sinatra-style routing, built-in validators, file serving, and middleware
9
+ composition for defining your API, plus multi-worker process management,
10
10
  rolling restarts, automatic TLS certificates, mDNS service discovery, and
11
- request load balancing all in a single 1,200-line file with zero external
11
+ request load balancing for running it in production all with zero external
12
12
  dependencies.
13
13
 
14
14
  ## Features
15
15
 
16
16
  - **Multi-worker architecture** — Automatic worker spawning based on CPU cores
17
- - **Hot module reloading** — File-watch based reloading with `-w` flag
17
+ - **Hot module reloading** — Watches `*.rip` files by default, rolling restarts on change
18
18
  - **Rolling restarts** — Zero-downtime deployments
19
19
  - **Automatic HTTPS** — TLS with mkcert or self-signed certificates
20
20
  - **mDNS discovery** — `.local` hostname advertisement
21
21
  - **Request queue** — Built-in request buffering and load balancing
22
22
  - **Built-in dashboard** — Server status UI at `rip.local`
23
- - **Powered by @rip-lang/api** — Runs any Rip API app
23
+ - **Unified package** — Web framework + production server in one
24
24
 
25
25
  | File | Lines | Role |
26
26
  |------|-------|------|
27
- | `server.rip` | ~1,210 | Complete server: CLI, workers, load balancing, TLS, mDNS |
27
+ | `api.rip` | ~662 | Core framework: routing, validation, `read()`, `session`, `@send`, server |
28
+ | `middleware.rip` | ~559 | Built-in middleware: cors, logger, sessions, compression, security, serve |
29
+ | `server.rip` | ~1,210 | Process manager: CLI, workers, load balancing, TLS, mDNS |
28
30
  | `server.html` | ~420 | Built-in dashboard UI |
29
31
 
30
- > **See Also**: For the API framework, see [@rip-lang/api](../api/README.md). For the DuckDB server, see [@rip-lang/db](../db/README.md).
32
+ > **See Also**: For the DuckDB server, see [@rip-lang/db](../db/README.md).
31
33
 
32
34
  ## Quick Start
33
35
 
@@ -37,27 +39,24 @@ dependencies.
37
39
  # Local (per-project)
38
40
  bun add @rip-lang/server
39
41
 
40
- # Global (use rip-server from anywhere)
42
+ # Global
41
43
  bun add -g rip-lang @rip-lang/server
42
44
  ```
43
45
 
44
46
  ### Running Your App
45
47
 
46
48
  ```bash
47
- # From your app directory (uses ./index.rip by default)
48
- rip-server
49
-
50
- # With file watching (recommended for development)
51
- rip-server -w
49
+ # From your app directory (uses ./index.rip, watches *.rip)
50
+ rip serve
52
51
 
53
52
  # Name your app (for mDNS: myapp.local)
54
- rip-server myapp
53
+ rip serve myapp
55
54
 
56
55
  # Explicit entry file
57
- rip-server ./app.rip
56
+ rip serve ./app.rip
58
57
 
59
58
  # HTTP only mode
60
- rip-server http
59
+ rip serve http
61
60
  ```
62
61
 
63
62
  ### Example App
@@ -65,7 +64,7 @@ rip-server http
65
64
  Create `index.rip`:
66
65
 
67
66
  ```coffee
68
- import { get, read, start } from '@rip-lang/api'
67
+ import { get, read, start } from '@rip-lang/server'
69
68
 
70
69
  get '/', ->
71
70
  'Hello from Rip Server!'
@@ -83,7 +82,7 @@ start()
83
82
  Run it:
84
83
 
85
84
  ```bash
86
- rip-server -w
85
+ rip serve
87
86
  ```
88
87
 
89
88
  Test it:
@@ -102,22 +101,677 @@ curl http://localhost/status
102
101
  # {"status":"healthy","app":"myapp","workers":5,"ports":{"https":443}}
103
102
  ```
104
103
 
104
+ ## The `read()` Function
105
+
106
+ A validation and parsing powerhouse that eliminates 90% of API boilerplate.
107
+
108
+ ### Basic Patterns
109
+
110
+ ```coffee
111
+ # Required field (throws if missing)
112
+ email = read 'email', 'email!'
113
+
114
+ # Optional field (returns null if missing)
115
+ phone = read 'phone', 'phone'
116
+
117
+ # With default value
118
+ role = read 'role', ['admin', 'user'], 'user'
119
+
120
+ # Get entire payload
121
+ data = read()
122
+ ```
123
+
124
+ ### Range Validation
125
+
126
+ The `[min, max]` syntax works for both numbers and string lengths:
127
+
128
+ ```coffee
129
+ # Numbers: value range
130
+ age = read 'age', 'int', [18, 120] # Between 18 and 120
131
+ priority = read 'priority', 'int', [1, 10] # 1-10 range
132
+
133
+ # Strings: length range
134
+ username = read 'username', 'string', [3, 20] # 3-20 characters
135
+ bio = read 'bio', 'string', [0, 500] # Up to 500 chars
136
+
137
+ # Named parameters
138
+ views = read 'views', 'int', min: 0 # Non-negative integer
139
+ discount = read 'discount', 'float', max: 100 # Up to 100
140
+ ```
141
+
142
+ ### Enumeration Validation
143
+
144
+ ```coffee
145
+ # Must be one of these values
146
+ role = read 'role', ['admin', 'user', 'guest']
147
+ status = read 'status', ['pending', 'active', 'closed']
148
+ ```
149
+
150
+ ### Regex Validation
151
+
152
+ ```coffee
153
+ # Custom pattern matching
154
+ code = read 'code', /^[A-Z]{3,6}$/
155
+ ```
156
+
157
+ ## Built-in Validators
158
+
159
+ `@rip-lang/server` includes 37 validators for every common API need:
160
+
161
+ ### Numbers & Money
162
+ ```coffee
163
+ id = read 'user_id', 'id!' # Positive integer (1+)
164
+ count = read 'count', 'whole' # Non-negative integer (0+)
165
+ price = read 'price', 'float' # Decimal number
166
+ cost = read 'cost', 'money' # Banker's rounding to cents
167
+ ```
168
+
169
+ ### Text Processing
170
+ ```coffee
171
+ title = read 'title', 'string' # Collapses whitespace
172
+ bio = read 'bio', 'text' # Light cleanup
173
+ name = read 'name', 'name' # Trims and normalizes
174
+ ```
175
+
176
+ ### Contact Information
177
+ ```coffee
178
+ email = read 'email', 'email' # Valid email format
179
+ phone = read 'phone', 'phone' # US phone → (555) 123-4567
180
+ address = read 'address', 'address' # Trimmed address
181
+ ```
182
+
183
+ ### Geographic Data
184
+ ```coffee
185
+ state = read 'state', 'state' # Two-letter → uppercase
186
+ zip = read 'zip', 'zip' # 5-digit zip
187
+ zipplus4 = read 'zip', 'zipplus4' # 12345-6789 format
188
+ ```
189
+
190
+ ### Identity & Security
191
+ ```coffee
192
+ ssn = read 'ssn', 'ssn' # SSN → digits only
193
+ sex = read 'gender', 'sex' # m/f/o
194
+ username = read 'username', 'username' # 3-20 chars, lowercase
195
+ ```
196
+
197
+ ### Web & Technical
198
+ ```coffee
199
+ url = read 'website', 'url' # Valid URL
200
+ ip = read 'ip_address', 'ip' # IPv4 address
201
+ mac = read 'mac', 'mac' # MAC address
202
+ color = read 'color', 'color' # Hex color → #abc123
203
+ uuid = read 'user_id', 'uuid' # UUID format
204
+ semver = read 'version', 'semver' # Semantic version
205
+ ```
206
+
207
+ ### Time & Date
208
+ ```coffee
209
+ date = read 'date', 'date' # YYYY-MM-DD
210
+ time = read 'time', 'time' # HH:MM or HH:MM:SS (24-hour)
211
+ time12 = read 'time', 'time12' # 12-hour with am/pm
212
+ ```
213
+
214
+ ### Boolean & Collections
215
+ ```coffee
216
+ active = read 'active', 'truthy' # true/t/1/yes/y/on → true
217
+ inactive = read 'off', 'falsy' # false/f/0/no/n/off → true
218
+ flag = read 'flag', 'bool' # Either → boolean
219
+ tags = read 'tags', 'array' # Must be array
220
+ config = read 'config', 'hash' # Must be object
221
+ settings = read 'data', 'json' # Parse JSON string
222
+ ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
223
+ slug = read 'slug', 'slug' # URL-safe slug
224
+ ```
225
+
226
+ ### Custom Validators
227
+
228
+ ```coffee
229
+ import { registerValidator, read } from '@rip-lang/server'
230
+
231
+ registerValidator 'postalCode', (v) ->
232
+ if v =~ /^[A-Z]\d[A-Z] \d[A-Z]\d$/i
233
+ _[0].toUpperCase()
234
+ else
235
+ null
236
+
237
+ # Now use it
238
+ code = read 'postal', 'postalCode!'
239
+ ```
240
+
241
+ ## Routing
242
+
243
+ ### HTTP Methods
244
+
245
+ ```coffee
246
+ import { get, post, put, patch, del, all } from '@rip-lang/server'
247
+
248
+ get '/users' -> listUsers!
249
+ post '/users' -> createUser!
250
+ get '/users/:id' -> getUser!
251
+ put '/users/:id' -> updateUser!
252
+ patch '/users/:id' -> patchUser!
253
+ del '/users/:id' -> deleteUser!
254
+ all '/health' -> 'ok' # All methods
255
+ ```
256
+
257
+ ### Path Parameters
258
+
259
+ ```coffee
260
+ # Basic parameters
261
+ get '/users/:id' ->
262
+ id = read 'id', 'id!'
263
+ { id }
264
+
265
+ # Multiple parameters
266
+ get '/users/:userId/posts/:postId' ->
267
+ userId = read 'userId', 'id!'
268
+ postId = read 'postId', 'id!'
269
+ { userId, postId }
270
+
271
+ # Custom patterns
272
+ get '/files/:name{[a-z]+\\.txt}' ->
273
+ name = read 'name'
274
+ { file: name }
275
+
276
+ # Wildcards
277
+ get '/static/*', (env) ->
278
+ { path: env.req.path }
279
+ ```
280
+
281
+ ### Route Grouping
282
+
283
+ ```coffee
284
+ import { prefix } from '@rip-lang/server'
285
+
286
+ prefix '/api/v1' ->
287
+ get '/users' -> listUsers!
288
+ get '/posts' -> listPosts!
289
+
290
+ prefix '/api/v2' ->
291
+ get '/users' -> listUsersV2!
292
+ ```
293
+
294
+ ## Middleware
295
+
296
+ ### Built-in Middleware
297
+
298
+ Import from `@rip-lang/server/middleware`:
299
+
300
+ ```coffee
301
+ import { use } from '@rip-lang/server'
302
+ import { cors, logger, compress, sessions, secureHeaders, timeout, bodyLimit } from '@rip-lang/server/middleware'
303
+
304
+ # Logging
305
+ use logger()
306
+ use logger format: 'tiny' # Minimal output
307
+ use logger format: 'dev' # Colorized (default)
308
+ use logger skip: (c) -> c.req.path is '/health'
309
+
310
+ # CORS
311
+ use cors() # Allow all origins
312
+ use cors origin: 'https://myapp.com' # Specific origin
313
+ use cors origin: ['https://a.com', 'https://b.com']
314
+ use cors credentials: true, maxAge: 86400
315
+
316
+ # Compression (gzip/deflate)
317
+ use compress()
318
+ use compress threshold: 1024 # Min bytes to compress
319
+
320
+ # Security headers
321
+ use secureHeaders()
322
+ use secureHeaders hsts: true, contentSecurityPolicy: "default-src 'self'"
323
+
324
+ # Request limits
325
+ use timeout ms: 30000 # 30 second timeout
326
+ use bodyLimit maxSize: 1024 * 1024 # 1MB max body
327
+ ```
328
+
329
+ ### Middleware Options
330
+
331
+ | Middleware | Options |
332
+ |------------|---------|
333
+ | `logger()` | `format`, `skip`, `stream` |
334
+ | `cors()` | `origin`, `methods`, `headers`, `credentials`, `maxAge`, `exposeHeaders`, `preflight` |
335
+ | `compress()` | `threshold`, `encodings` |
336
+ | `sessions()` | `secret`, `name`, `maxAge`, `secure`, `httpOnly`, `sameSite` |
337
+ | `secureHeaders()` | `hsts`, `hstsMaxAge`, `contentSecurityPolicy`, `frameOptions`, `referrerPolicy` |
338
+ | `timeout()` | `ms`, `message`, `status` |
339
+ | `bodyLimit()` | `maxSize`, `message` |
340
+
341
+ ### Session Usage
342
+
343
+ ```coffee
344
+ import { get, use, before, session } from '@rip-lang/server'
345
+ import { sessions } from '@rip-lang/server/middleware'
346
+
347
+ # Sessions parses cookies directly from request headers
348
+ use sessions secret: process.env.SESSION_SECRET
349
+
350
+ before ->
351
+ session.views ?= 0
352
+ session.views += 1
353
+
354
+ get '/profile' ->
355
+ { userId: session.userId, views: session.views }
356
+
357
+ get '/login' ->
358
+ session.userId = 123
359
+ { loggedIn: true }
360
+
361
+ get '/logout' ->
362
+ delete session.userId
363
+ { loggedOut: true }
364
+ ```
365
+
366
+ The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
367
+
368
+ **Security note:** Without `secret`, sessions use plain base64 (dev only). With `secret`, sessions are HMAC-SHA256 signed (tamper-proof). Always set `secret` in production.
369
+
370
+ ### CORS with Preflight
371
+
372
+ ```coffee
373
+ import { use } from '@rip-lang/server'
374
+ import { cors } from '@rip-lang/server/middleware'
375
+
376
+ # Handle OPTIONS early (before routes are matched)
377
+ use cors origin: 'https://myapp.com', preflight: true
378
+ ```
379
+
380
+ ### Custom Middleware
381
+
382
+ ```coffee
383
+ # Authentication middleware
384
+ use (c, next) ->
385
+ token = @req.header 'Authorization'
386
+ unless token
387
+ return @json { error: 'Unauthorized' }, 401
388
+ @user = validateToken!(token)
389
+ await next()
390
+
391
+ # Timing middleware
392
+ use (c, next) ->
393
+ start = Date.now()
394
+ await next()
395
+ @header 'X-Response-Time', "#{Date.now() - start}ms"
396
+ ```
397
+
398
+ ### Request Lifecycle Filters
399
+
400
+ Three filters run at different stages: `raw` → `before` → handler → `after`
401
+
402
+ ```coffee
403
+ import { raw, before, after, get } from '@rip-lang/server'
404
+
405
+ # Runs first — modify raw request before body parsing
406
+ raw (req) ->
407
+ if req.headers.get('X-Raw-SQL') is 'true'
408
+ req.headers.set 'content-type', 'text/plain'
409
+
410
+ skipPaths = ['/favicon.ico', '/ping', '/health']
411
+
412
+ # Runs before handler (after body parsing)
413
+ before ->
414
+ @start = Date.now()
415
+ @silent = @req.path in skipPaths
416
+ unless @req.header 'Authorization'
417
+ return @json { error: 'Unauthorized' }, 401
418
+
419
+ # Runs after handler
420
+ after ->
421
+ return if @silent
422
+ console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
423
+ ```
424
+
425
+ **Note:** `raw` receives the native `Request` object (before parsing). `before` and `after` use `@` to access the context.
426
+
427
+ **How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
428
+ - `@req` — Request object
429
+ - `@json()`, `@text()`, `@html()`, `@redirect()`, `@send()` — Response helpers
430
+ - `@header()` — Response header modifier
431
+ - `@anything` — Custom per-request state
432
+
433
+ **Imports that work anywhere** (via AsyncLocalStorage or Proxy):
434
+ - `read` — Validated request parameters
435
+ - `session` — Session data (if middleware enabled)
436
+ - `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
437
+
438
+ ## Context Object
439
+
440
+ Use `@` to access the context directly — no parameter needed:
441
+
442
+ ### Response Helpers
443
+
444
+ ```coffee
445
+ get '/demo' ->
446
+ # JSON response
447
+ @json { data: 'value' }
448
+ @json { data: 'value' }, 201 # With status
449
+ @json { data: 'value' }, 200, { 'X-Custom': 'header' }
450
+
451
+ # Text response
452
+ @text 'Hello'
453
+ @text 'Created', 201
454
+
455
+ # HTML response
456
+ @html '<h1>Hello</h1>'
457
+
458
+ # Redirect
459
+ @redirect '/new-location'
460
+ @redirect '/new-location', 301 # Permanent
461
+
462
+ # Raw body
463
+ @body data, 200, { 'Content-Type': 'application/octet-stream' }
464
+
465
+ # File serving (auto-detected MIME type via Bun.file)
466
+ @send 'public/style.css' # text/css
467
+ @send 'data/export.json', 'application/json' # explicit type
468
+ ```
469
+
470
+ ### Request Helpers
471
+
472
+ ```coffee
473
+ get '/info' ->
474
+ # Path and query parameters — use read() for validation!
475
+ id = read 'id', 'id!'
476
+ q = read 'q'
477
+
478
+ # Headers
479
+ auth = @req.header 'Authorization'
480
+ allHeaders = @req.header()
481
+
482
+ # Body (async)
483
+ json = @req.json!
484
+ text = @req.text!
485
+ form = @req.formData!
486
+ parsed = @req.parseBody!
487
+
488
+ # Raw request
489
+ @req.raw # Native Request object
490
+ @req.method # 'GET', 'POST', etc.
491
+ @req.url # Full URL
492
+ @req.path # Path only
493
+ ```
494
+
495
+ ### Request-Scoped State
496
+
497
+ ```coffee
498
+ # Store data for later middleware/handlers
499
+ use (c, next) ->
500
+ @user = { id: 1, name: 'Alice' }
501
+ @startTime = Date.now()
502
+ await next()
503
+
504
+ get '/profile' ->
505
+ @json @user
506
+ ```
507
+
508
+ ## File Serving
509
+
510
+ ### `@send(path, type?)`
511
+
512
+ Serve a file with auto-detected MIME type. Uses `Bun.file()` internally for
513
+ efficient streaming — the file is never buffered in memory.
514
+
515
+ ```coffee
516
+ # Auto-detected content type (30+ extensions supported)
517
+ get '/css/*', -> @send "css/#{@req.path.slice(5)}"
518
+
519
+ # Explicit content type
520
+ get '/files/*', -> @send "uploads/#{@req.path.slice(7)}", 'application/octet-stream'
521
+
522
+ # SPA fallback — serve index.html for all unmatched routes
523
+ notFound -> @send 'index.html', 'text/html; charset=UTF-8'
524
+ ```
525
+
526
+ ### `mimeType(path)`
527
+
528
+ Exported utility that returns the MIME type for a file path:
529
+
530
+ ```coffee
531
+ import { mimeType } from '@rip-lang/server'
532
+
533
+ mimeType 'style.css' # 'text/css; charset=UTF-8'
534
+ mimeType 'app.js' # 'application/javascript'
535
+ mimeType 'photo.png' # 'image/png'
536
+ mimeType 'data.xyz' # 'application/octet-stream'
537
+ ```
538
+
539
+ ## Error Handling
540
+
541
+ ### Custom Error Handler
542
+
543
+ ```coffee
544
+ import { onError } from '@rip-lang/server'
545
+
546
+ onError (err, c) ->
547
+ console.error 'Error:', err
548
+ c.json { error: err.message }, err.status or 500
549
+ ```
550
+
551
+ ### Custom 404 Handler
552
+
553
+ ```coffee
554
+ import { notFound } from '@rip-lang/server'
555
+
556
+ notFound (c) ->
557
+ c.json { error: 'Not found', path: c.req.path }, 404
558
+ ```
559
+
560
+ ## Server Options
561
+
562
+ ### Basic Server
563
+
564
+ ```coffee
565
+ import { start } from '@rip-lang/server'
566
+
567
+ start port: 3000
568
+ start port: 3000, host: '0.0.0.0'
569
+ ```
570
+
571
+ ### Handler Only (for custom servers)
572
+
573
+ ```coffee
574
+ import { startHandler } from '@rip-lang/server'
575
+
576
+ export default startHandler()
577
+ ```
578
+
579
+ ### App Pattern
580
+
581
+ ```coffee
582
+ import { App, get, post } from '@rip-lang/server'
583
+
584
+ export default App ->
585
+ get '/', -> 'Hello'
586
+ post '/echo', -> read()
587
+ ```
588
+
589
+ ## Context Utilities
590
+
591
+ ### ctx()
592
+
593
+ Get the current request context from anywhere (via AsyncLocalStorage):
594
+
595
+ ```coffee
596
+ import { ctx } from '@rip-lang/server'
597
+
598
+ logRequest = ->
599
+ c = ctx()
600
+ console.log "#{c.req.method} #{c.req.path}" if c
601
+
602
+ get '/demo' ->
603
+ logRequest()
604
+ { ok: true }
605
+ ```
606
+
607
+ ### resetGlobals()
608
+
609
+ Reset all global state (routes, middleware, filters). Useful for testing:
610
+
611
+ ```coffee
612
+ import { resetGlobals, get, start } from '@rip-lang/server'
613
+
614
+ beforeEach ->
615
+ resetGlobals()
616
+
617
+ get '/test', -> { test: true }
618
+ ```
619
+
620
+ ## Utility Functions
621
+
622
+ ### isBlank
623
+
624
+ ```coffee
625
+ import { isBlank } from '@rip-lang/server'
626
+
627
+ isBlank null # true
628
+ isBlank undefined # true
629
+ isBlank '' # true
630
+ isBlank ' ' # true
631
+ isBlank [] # true
632
+ isBlank {} # true
633
+ isBlank false # true
634
+ isBlank 'hello' # false
635
+ isBlank [1, 2] # false
636
+ ```
637
+
638
+ ### toName
639
+
640
+ Advanced name formatting with intelligent capitalization:
641
+
642
+ ```coffee
643
+ import { toName } from '@rip-lang/server'
644
+
645
+ toName 'john doe' # 'John Doe'
646
+ toName 'JANE SMITH' # 'Jane Smith'
647
+ toName "o'brien" # "O'Brien"
648
+ toName 'mcdonald' # 'McDonald'
649
+ toName 'los angeles', 'address' # 'Los Angeles'
650
+ ```
651
+
652
+ ### toPhone
653
+
654
+ US phone number formatting:
655
+
656
+ ```coffee
657
+ import { toPhone } from '@rip-lang/server'
658
+
659
+ toPhone '5551234567' # '(555) 123-4567'
660
+ toPhone '555-123-4567' # '(555) 123-4567'
661
+ toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
662
+ toPhone '+1 555 123 4567' # '(555) 123-4567'
663
+ ```
664
+
665
+ ## Migration from Hono
666
+
667
+ ### Before (Hono)
668
+
669
+ ```coffee
670
+ import { Hono } from 'hono'
671
+
672
+ app = new Hono()
673
+ app.get '/users/:id', (c) ->
674
+ id = c.req.param 'id'
675
+ c.json { id }
676
+
677
+ export default app
678
+ ```
679
+
680
+ ### After (@rip-lang/server)
681
+
682
+ ```coffee
683
+ import { get, read, startHandler } from '@rip-lang/server'
684
+
685
+ get '/users/:id', ->
686
+ id = read 'id', 'id!'
687
+ { id }
688
+
689
+ export default startHandler()
690
+ ```
691
+
692
+ ### API Compatibility
693
+
694
+ | Hono | @rip-lang/server |
695
+ |------|------------------|
696
+ | `app.get(path, handler)` | `get path, handler` |
697
+ | `app.post(path, handler)` | `post path, handler` |
698
+ | `app.use(middleware)` | `use middleware` |
699
+ | `app.basePath(path)` | `prefix path, -> ...` |
700
+ | `c.json(data)` | `@json(data)` or return `{ data }` |
701
+ | `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
702
+ | `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
703
+
704
+ ## Real-World Example
705
+
706
+ ```coffee
707
+ import { get, post, put, del, use, read, start, before, after, onError } from '@rip-lang/server'
708
+ import { logger } from '@rip-lang/server/middleware'
709
+
710
+ use logger()
711
+
712
+ before ->
713
+ @start = Date.now()
714
+
715
+ after ->
716
+ console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
717
+
718
+ onError (err) ->
719
+ @json { error: err.message }, err.status or 500
720
+
721
+ get '/', ->
722
+ { name: 'My API', version: '1.0' }
723
+
724
+ get '/users', ->
725
+ page = read 'page', 'int', [1, 100]
726
+ limit = read 'limit', 'int', [1, 50]
727
+ users = db.listUsers! page or 1, limit or 10
728
+ { users, page, limit }
729
+
730
+ get '/users/:id', ->
731
+ id = read 'id', 'id!'
732
+ user = db.getUser!(id)
733
+ unless user
734
+ throw { message: 'User not found', status: 404 }
735
+ { user }
736
+
737
+ post '/users', ->
738
+ email = read 'email', 'email!'
739
+ name = read 'name', 'string', [1, 100]
740
+ phone = read 'phone', 'phone'
741
+ user = db.createUser! { email, name, phone }
742
+ { user, created: true }
743
+
744
+ put '/users/:id', ->
745
+ id = read 'id', 'id!'
746
+ email = read 'email', 'email'
747
+ name = read 'name', 'string', [1, 100]
748
+ user = db.updateUser! id, { email, name }
749
+ { user, updated: true }
750
+
751
+ del '/users/:id', ->
752
+ id = read 'id', 'id!'
753
+ db.deleteUser!(id)
754
+ { deleted: true }
755
+
756
+ start port: 3000
757
+ ```
758
+
105
759
  ## App Path & Naming
106
760
 
107
761
  ### Entry File Resolution
108
762
 
109
- When you run `rip-server`, it looks for your app's entry file:
763
+ When you run `rip serve`, it looks for your app's entry file:
110
764
 
111
765
  ```bash
112
766
  # No arguments: looks for index.rip (or index.ts) in current directory
113
- rip-server
767
+ rip serve
114
768
 
115
769
  # Directory path: looks for index.rip (or index.ts) in that directory
116
- rip-server ./myapp/
770
+ rip serve ./myapp/
117
771
 
118
772
  # Explicit file: uses that file directly
119
- rip-server ./app.rip
120
- rip-server ./src/server.ts
773
+ rip serve ./app.rip
774
+ rip serve ./src/server.ts
121
775
  ```
122
776
 
123
777
  ### App Naming
@@ -126,44 +780,36 @@ The **app name** is used for mDNS discovery (e.g., `myapp.local`) and logging. I
126
780
 
127
781
  ```bash
128
782
  # Default: current directory name becomes app name
129
- ~/projects/api$ rip-server # app name = "api"
783
+ ~/projects/api$ rip serve # app name = "api"
130
784
 
131
785
  # Explicit name: pass a name that's not a file path
132
- rip-server myapp # app name = "myapp"
786
+ rip serve myapp # app name = "myapp"
133
787
 
134
788
  # With aliases: name@alias1,alias2
135
- rip-server myapp@api,backend # accessible at myapp.local, api.local, backend.local
789
+ rip serve myapp@api,backend # accessible at myapp.local, api.local, backend.local
136
790
 
137
791
  # Path with alias
138
- rip-server ./app.rip@myapp # explicit file + custom app name
792
+ rip serve ./app.rip@myapp # explicit file + custom app name
139
793
  ```
140
794
 
141
795
  **Examples:**
142
796
 
143
797
  ```bash
144
798
  # In ~/projects/api/ with index.rip
145
- rip-server # app = "api", entry = ./index.rip
146
- rip-server -w # same, with file watching
147
- rip-server myapp # app = "myapp", entry = ./index.rip
148
- rip-server myapp -w # same, with file watching
149
- rip-server ./server.rip # app = "api", entry = ./server.rip
150
- rip-server ./server.rip@myapp # app = "myapp", entry = ./server.rip
799
+ rip serve # app = "api", entry = ./index.rip
800
+ rip serve myapp # app = "myapp", entry = ./index.rip
801
+ rip serve ./server.rip # app = "api", entry = ./server.rip
802
+ rip serve ./server.rip@myapp # app = "myapp", entry = ./server.rip
151
803
  ```
152
804
 
153
805
  ## File Watching
154
806
 
155
- ### Development Mode with `-w`/`--watch`
156
-
157
- The `-w` flag enables **directory watching** — any `.rip` file change in your app directory triggers an automatic hot reload:
807
+ Directory watching is **on by default** — any `.rip` file change in your app directory triggers an automatic rolling restart. Use `--watch=<glob>` to customize the pattern, or `--static` to disable watching entirely.
158
808
 
159
809
  ```bash
160
- # Watch all .rip files (default pattern: *.rip)
161
- rip-server -w
162
- rip-server --watch
163
-
164
- # Watch a custom pattern
165
- rip-server -w=*.ts
166
- rip-server --watch=*.tsx
810
+ rip serve # Watches *.rip (default)
811
+ rip serve --watch=*.ts # Watch TypeScript files instead
812
+ rip serve --static # No watching, no hot reload (production)
167
813
  ```
168
814
 
169
815
  **How it works:**
@@ -171,48 +817,17 @@ rip-server --watch=*.tsx
171
817
  1. Uses OS-native file watching (FSEvents on macOS, inotify on Linux)
172
818
  2. Watches the entire app directory recursively
173
819
  3. When a matching file changes, touches the entry file
174
- 4. The existing hot-reload mechanism detects the change and does a rolling restart
175
-
176
- **This is efficient:**
177
-
178
- - Single watcher in the main process (not per-worker)
179
- - No polling — OS notifies on changes
180
- - Zero overhead when files aren't changing
181
-
182
- **Examples:**
183
-
184
- ```bash
185
- # Typical development setup
186
- rip-server -w # Watch *.rip files
187
-
188
- # TypeScript project
189
- rip-server -w=*.ts # Watch *.ts files
820
+ 4. The hot-reload mechanism detects the mtime change and does a rolling restart
190
821
 
191
- # React/frontend project
192
- rip-server -w=*.tsx # Watch *.tsx files
193
-
194
- # Multiple concerns? Just use the broader pattern
195
- rip-server -w=*.rip # Only Rip files (default)
196
- ```
197
-
198
- **Without `-w`:** Only the entry file (`index.rip`) is watched. Changes to imported files won't trigger reload unless you also touch the entry file.
822
+ This is a single kernel-level file descriptor in the main process — no polling, zero overhead when files aren't changing.
199
823
 
200
824
  ## CLI Reference
201
825
 
202
826
  ### Basic Syntax
203
827
 
204
828
  ```bash
205
- rip-server [flags] [app-path] [app-name]
206
- rip-server [flags] [app-path]@<alias1>,<alias2>,...
207
- ```
208
-
209
- ### Getting Help
210
-
211
- ```bash
212
- rip-server -h # Show help
213
- rip-server --help # Show help
214
- rip-server -v # Show version
215
- rip-server --version # Show version
829
+ rip serve [flags] [app-path] [app-name]
830
+ rip serve [flags] [app-path]@<alias1>,<alias2>,...
216
831
  ```
217
832
 
218
833
  ### Flags
@@ -221,15 +836,14 @@ rip-server --version # Show version
221
836
  |------|-------------|---------|
222
837
  | `-h`, `--help` | Show help and exit | — |
223
838
  | `-v`, `--version` | Show version and exit | — |
224
- | `-w`, `--watch` | Watch `*.rip` files for changes | Disabled |
225
- | `-w=<glob>`, `--watch=<glob>` | Watch custom pattern (e.g., `*.ts`) | — |
839
+ | `--watch=<glob>` | Watch glob pattern | `*.rip` |
840
+ | `--static` | Disable hot reload and file watching | — |
226
841
  | `--env=<mode>` | Environment mode (`dev`, `prod`) | `development` |
227
842
  | `--debug` | Enable debug logging | Disabled |
228
- | `--static` | Disable hot reload (production) | Hot reload enabled |
229
843
  | `http` | HTTP-only mode (no HTTPS) | HTTPS enabled |
230
844
  | `https` | HTTPS mode (explicit) | Auto |
231
- | `http:<port>` | Set HTTP port | 80 or 5700 |
232
- | `https:<port>` | Set HTTPS port | 443 or 5700 |
845
+ | `http:<port>` | Set HTTP port | 80, fallback 3000 |
846
+ | `https:<port>` | Set HTTPS port | 443, fallback 3443 |
233
847
  | `w:<n>` | Worker count (`auto`, `half`, `2x`, `3x`, or number) | `half` of cores |
234
848
  | `r:<reqs>,<secs>s` | Restart policy: requests, seconds (e.g., `5000,3600s`) | `10000,3600s` |
235
849
  | `--cert=<path>` | TLS certificate path | Auto-generated |
@@ -243,42 +857,39 @@ rip-server --version # Show version
243
857
  ### Subcommands
244
858
 
245
859
  ```bash
246
- # Stop running server
247
- rip-server stop
248
-
249
- # List registered hosts
250
- rip-server list
860
+ rip serve stop # Stop running server
861
+ rip serve list # List registered hosts
251
862
  ```
252
863
 
253
864
  ### Examples
254
865
 
255
866
  ```bash
256
- # Development with file watching (recommended)
257
- rip-server -w
867
+ # Development (default: watches *.rip, HTTPS, hot reload)
868
+ rip serve
258
869
 
259
- # Development: HTTP on any available port
260
- rip-server http
870
+ # HTTP only
871
+ rip serve http
261
872
 
262
- # Development: HTTPS with mkcert
263
- rip-server --auto-tls
873
+ # HTTPS with mkcert
874
+ rip serve --auto-tls
264
875
 
265
- # Production: 8 workers, HTTPS, no hot reload
266
- rip-server --env=prod --static w:8
876
+ # Production: 8 workers, no hot reload
877
+ rip serve --static w:8
267
878
 
268
879
  # Custom port
269
- rip-server http:3000
880
+ rip serve http:3000
270
881
 
271
882
  # With mDNS aliases (accessible as myapp.local and api.local)
272
- rip-server myapp@api
883
+ rip serve myapp@api
273
884
 
274
- # Watch TypeScript files
275
- rip-server -w=*.ts
885
+ # Watch TypeScript files instead of Rip
886
+ rip serve --watch=*.ts
276
887
 
277
- # Debug mode to troubleshoot issues
278
- rip-server --debug -w
888
+ # Debug mode
889
+ rip serve --debug
279
890
 
280
891
  # Restart workers after 5000 requests or 1 hour
281
- rip-server r:5000,3600s
892
+ rip serve r:5000,3600s
282
893
  ```
283
894
 
284
895
  ## Architecture
@@ -328,10 +939,12 @@ When `RIP_SETUP_MODE=1` is set, the same file runs the one-time setup phase. Whe
328
939
 
329
940
  ### Hot Reloading
330
941
 
331
- Two layers of hot reload work together in development:
942
+ Two layers of hot reload work together by default:
332
943
 
333
- - **API changes** (`-w` flag) — The Manager watches for `.rip` file changes in the API directory and triggers rolling worker restarts (zero downtime, server-side).
334
- - **UI changes** (`watch: true` in `serve`) — Workers register their app's component directories with the Manager via the control socket. The Manager watches those directories and broadcasts SSE reload events to connected browsers (client-side). SSE connections are held by the long-lived Server process, not by workers.
944
+ - **API changes** — The Manager watches for `.rip` file changes in the app directory and triggers rolling worker restarts (zero downtime, server-side).
945
+ - **UI changes** (`watch: true` in `serve` middleware) — Workers register their component directories with the Manager via the control socket. The Manager watches those directories and broadcasts SSE reload events to connected browsers (client-side).
946
+
947
+ SSE connections are held by the long-lived Server process, not by recyclable workers, ensuring stable hot-reload connections. Each app prefix gets its own SSE pool for multi-app isolation.
335
948
 
336
949
  Use `--static` in production to disable hot reload entirely.
337
950
 
@@ -365,7 +978,7 @@ Certificates are stored in `~/.rip/certs/`.
365
978
  ### Custom Certificates
366
979
 
367
980
  ```bash
368
- rip-server --cert=/path/to/cert.pem --key=/path/to/key.pem
981
+ rip serve --cert=/path/to/cert.pem --key=/path/to/key.pem
369
982
  ```
370
983
 
371
984
  ## mDNS Service Discovery
@@ -374,10 +987,10 @@ The server automatically advertises itself via mDNS (Bonjour/Zeroconf):
374
987
 
375
988
  ```bash
376
989
  # App accessible at myapp.local
377
- rip-server myapp
990
+ rip serve myapp
378
991
 
379
992
  # Multiple aliases
380
- rip-server myapp@api,backend
993
+ rip serve myapp@api,backend
381
994
  ```
382
995
 
383
996
  Requires `dns-sd` (available on macOS by default).
@@ -386,17 +999,17 @@ Requires `dns-sd` (available on macOS by default).
386
999
 
387
1000
  Your app must provide a fetch handler. Three patterns are supported:
388
1001
 
389
- ### Pattern 1: Use `@rip-lang/api` with `start()` (Recommended)
1002
+ ### Pattern 1: Use `@rip-lang/server` with `start()` (Recommended)
390
1003
 
391
1004
  ```coffee
392
- import { get, start } from '@rip-lang/api'
1005
+ import { get, start } from '@rip-lang/server'
393
1006
 
394
1007
  get '/', -> 'Hello!'
395
1008
 
396
1009
  start()
397
1010
  ```
398
1011
 
399
- The `start()` function automatically detects when running under `rip-server` and registers the handler.
1012
+ The `start()` function automatically detects when running under `rip serve` and registers the handler.
400
1013
 
401
1014
  ### Pattern 2: Export fetch function directly
402
1015
 
@@ -414,7 +1027,7 @@ export default
414
1027
 
415
1028
  ## One-Time Setup
416
1029
 
417
- If a `setup.rip` file exists next to your entry file, rip-server runs it
1030
+ If a `setup.rip` file exists next to your entry file, `rip serve` runs it
418
1031
  automatically **once** before spawning any workers. This is ideal for database
419
1032
  migrations, table creation, and seeding.
420
1033
 
@@ -464,7 +1077,7 @@ The server includes a built-in dashboard accessible at `http://rip.local/` (when
464
1077
  - **Registered Hosts** — All mDNS aliases being advertised
465
1078
  - **Server Ports** — HTTP/HTTPS port configuration
466
1079
 
467
- The dashboard uses the same mDNS infrastructure as your app, so it's always available at `rip.local` when any rip-server instance is running.
1080
+ The dashboard uses the same mDNS infrastructure as your app, so it's always available at `rip.local` when any `rip serve` instance is running.
468
1081
 
469
1082
  ## Troubleshooting
470
1083
 
@@ -474,13 +1087,13 @@ The dashboard uses the same mDNS infrastructure as your app, so it's always avai
474
1087
 
475
1088
  **Workers keep restarting**: Use `--debug` (or `RIP_DEBUG=1`) to see import errors in your app.
476
1089
 
477
- **Changes not triggering reload**: Make sure you're using `-w` flag for directory watching, or touch your entry file manually.
1090
+ **Changes not triggering reload**: Ensure you're not using `--static`. Check that the file matches the watch pattern (default: `*.rip`).
478
1091
 
479
1092
  ## Serving Rip UI Apps
480
1093
 
481
1094
  Rip Server works seamlessly with the `serve` middleware for serving
482
1095
  reactive web applications with hot reload. The `serve` middleware handles
483
- framework files, page manifests, and SSE hot-reload — rip-server adds HTTPS,
1096
+ framework files, page manifests, and SSE hot-reload — `rip serve` adds HTTPS,
484
1097
  mDNS, multi-worker load balancing, and rolling restarts on top.
485
1098
 
486
1099
  ### Example: Rip UI App
@@ -488,8 +1101,8 @@ mDNS, multi-worker load balancing, and rolling restarts on top.
488
1101
  Create `index.rip`:
489
1102
 
490
1103
  ```coffee
491
- import { get, use, start, notFound } from '@rip-lang/api'
492
- import { serve } from '@rip-lang/api/serve'
1104
+ import { get, use, start, notFound } from '@rip-lang/server'
1105
+ import { serve } from '@rip-lang/server/middleware'
493
1106
 
494
1107
  dir = import.meta.dir
495
1108
 
@@ -505,7 +1118,7 @@ start()
505
1118
  Run it:
506
1119
 
507
1120
  ```bash
508
- rip-server -w
1121
+ rip serve
509
1122
  ```
510
1123
 
511
1124
  This gives you:
@@ -518,29 +1131,16 @@ This gives you:
518
1131
  - **Multi-worker** — load balanced across CPU cores
519
1132
  - **Rolling restarts** — zero-downtime file-watch reloading
520
1133
 
521
- ### How Hot Reload Works with rip-server
522
-
523
- When running with `-w`, two layers of hot reload work together:
524
-
525
- 1. **API hot reload** (`-w` flag) — The Manager watches for `.rip` file changes
526
- in the API directory and triggers rolling worker restarts (server-side).
527
- 2. **UI hot reload** (`watch: true`) — Workers register their component
528
- directories with the Manager via the control socket. The Manager watches
529
- those directories and tells the Server to broadcast SSE reload events to
530
- connected browsers (client-side).
531
-
532
- SSE connections are held by the long-lived Server process, not by recyclable
533
- workers, ensuring stable hot-reload connections. Each app prefix gets its own
534
- SSE pool for multi-app isolation.
1134
+ See [Hot Reloading](#hot-reloading) for details on how the two layers (API + UI) work together.
535
1135
 
536
1136
  ## Comparison with Other Servers
537
1137
 
538
- | Feature | rip-server | PM2 | Nginx |
539
- |---------|------------|-----|-------|
1138
+ | Feature | rip serve | PM2 | Nginx |
1139
+ |---------|-----------|-----|-------|
540
1140
  | Pure Rip | ✅ | ❌ | ❌ |
541
1141
  | Single File | ✅ (~1,200 lines) | ❌ | ❌ |
542
- | Hot Reload | ✅ | ✅ | ❌ |
543
- | Directory Watch | ✅ (`-w` flag) | ✅ | ❌ |
1142
+ | Hot Reload | ✅ (default) | ✅ | ❌ |
1143
+ | Directory Watch | ✅ (default) | ✅ | ❌ |
544
1144
  | Multi-Worker | ✅ | ✅ | ✅ |
545
1145
  | Auto HTTPS | ✅ | ❌ | ❌ |
546
1146
  | mDNS | ✅ | ❌ | ❌ |
@@ -563,7 +1163,5 @@ MIT
563
1163
 
564
1164
  ## Links
565
1165
 
566
- - [Rip Language](https://github.com/shreeve/rip-lang)
567
- - [@rip-lang/api](../api/README.md) — API framework (routing, middleware, `@send`)
568
- - [Rip](https://github.com/shreeve/rip-lang) — Compiler + reactive UI framework
1166
+ - [Rip Language](https://github.com/shreeve/rip-lang) — Compiler + reactive UI framework
569
1167
  - [Report Issues](https://github.com/shreeve/rip-lang/issues)