@rip-lang/server 1.2.10 → 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 +740 -142
- package/api.rip +662 -0
- package/middleware.rip +558 -0
- package/package.json +17 -8
- package/server.rip +26 -40
- package/tests/read.test.rip +254 -0
package/README.md
CHANGED
|
@@ -2,32 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
# Rip Server - @rip-lang/server
|
|
4
4
|
|
|
5
|
-
> **A
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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** —
|
|
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
|
-
- **
|
|
23
|
+
- **Unified package** — Web framework + production server in one
|
|
24
24
|
|
|
25
25
|
| File | Lines | Role |
|
|
26
26
|
|------|-------|------|
|
|
27
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
48
|
-
rip
|
|
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
|
|
53
|
+
rip serve myapp
|
|
55
54
|
|
|
56
55
|
# Explicit entry file
|
|
57
|
-
rip
|
|
56
|
+
rip serve ./app.rip
|
|
58
57
|
|
|
59
58
|
# HTTP only mode
|
|
60
|
-
rip
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
767
|
+
rip serve
|
|
114
768
|
|
|
115
769
|
# Directory path: looks for index.rip (or index.ts) in that directory
|
|
116
|
-
rip
|
|
770
|
+
rip serve ./myapp/
|
|
117
771
|
|
|
118
772
|
# Explicit file: uses that file directly
|
|
119
|
-
rip
|
|
120
|
-
rip
|
|
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
|
|
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
|
|
786
|
+
rip serve myapp # app name = "myapp"
|
|
133
787
|
|
|
134
788
|
# With aliases: name@alias1,alias2
|
|
135
|
-
rip
|
|
789
|
+
rip serve myapp@api,backend # accessible at myapp.local, api.local, backend.local
|
|
136
790
|
|
|
137
791
|
# Path with alias
|
|
138
|
-
rip
|
|
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
|
|
146
|
-
rip
|
|
147
|
-
rip
|
|
148
|
-
rip
|
|
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
|
-
|
|
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
|
-
#
|
|
161
|
-
rip
|
|
162
|
-
rip
|
|
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
|
|
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
|
-
|
|
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
|
|
206
|
-
rip
|
|
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
|
-
|
|
|
225
|
-
|
|
|
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
|
|
232
|
-
| `https:<port>` | Set HTTPS port | 443
|
|
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
|
|
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
|
|
257
|
-
rip
|
|
867
|
+
# Development (default: watches *.rip, HTTPS, hot reload)
|
|
868
|
+
rip serve
|
|
258
869
|
|
|
259
|
-
#
|
|
260
|
-
rip
|
|
870
|
+
# HTTP only
|
|
871
|
+
rip serve http
|
|
261
872
|
|
|
262
|
-
#
|
|
263
|
-
rip
|
|
873
|
+
# HTTPS with mkcert
|
|
874
|
+
rip serve --auto-tls
|
|
264
875
|
|
|
265
|
-
# Production: 8 workers,
|
|
266
|
-
rip
|
|
876
|
+
# Production: 8 workers, no hot reload
|
|
877
|
+
rip serve --static w:8
|
|
267
878
|
|
|
268
879
|
# Custom port
|
|
269
|
-
rip
|
|
880
|
+
rip serve http:3000
|
|
270
881
|
|
|
271
882
|
# With mDNS aliases (accessible as myapp.local and api.local)
|
|
272
|
-
rip
|
|
883
|
+
rip serve myapp@api
|
|
273
884
|
|
|
274
|
-
# Watch TypeScript files
|
|
275
|
-
rip
|
|
885
|
+
# Watch TypeScript files instead of Rip
|
|
886
|
+
rip serve --watch=*.ts
|
|
276
887
|
|
|
277
|
-
# Debug mode
|
|
278
|
-
rip
|
|
888
|
+
# Debug mode
|
|
889
|
+
rip serve --debug
|
|
279
890
|
|
|
280
891
|
# Restart workers after 5000 requests or 1 hour
|
|
281
|
-
rip
|
|
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
|
|
942
|
+
Two layers of hot reload work together by default:
|
|
332
943
|
|
|
333
|
-
- **API changes**
|
|
334
|
-
- **UI changes** (`watch: true` in `serve`) — Workers register their
|
|
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
|
|
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
|
|
990
|
+
rip serve myapp
|
|
378
991
|
|
|
379
992
|
# Multiple aliases
|
|
380
|
-
rip
|
|
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/
|
|
1002
|
+
### Pattern 1: Use `@rip-lang/server` with `start()` (Recommended)
|
|
390
1003
|
|
|
391
1004
|
```coffee
|
|
392
|
-
import { get, start } from '@rip-lang/
|
|
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
|
|
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
|
|
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
|
|
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**:
|
|
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
|
|
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/
|
|
492
|
-
import { serve } from '@rip-lang/
|
|
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
|
|
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
|
-
|
|
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
|
|
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 | ✅ (
|
|
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)
|