@rip-lang/server 1.2.11 → 1.3.1

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/server.rip CHANGED
@@ -280,7 +280,7 @@ parseFlags = (argv) ->
280
280
  httpIntent = hasHttpKeyword or httpPortToken?
281
281
  httpsIntent = true unless httpsIntent or httpIntent
282
282
 
283
- httpPort = if httpIntent then (httpPortToken ?? 0) else 0
283
+ httpPort = if httpIntent then (httpPortToken ?? bareIntPort ?? 0) else 0
284
284
  httpsPortDerived = if not httpIntent then (bareIntPort or httpsPortToken or 0) else null
285
285
 
286
286
  socketPrefixOverride = getKV('--socket-prefix=')
@@ -309,12 +309,8 @@ parseFlags = (argv) ->
309
309
  # Debug mode
310
310
  _debugMode = has('--debug')
311
311
 
312
- # Watch mode: -w, --watch, -w=glob, --watch=glob
313
- watchGlob = do ->
314
- return getKV('-w=') if getKV('-w=')?
315
- return getKV('--watch=') if getKV('--watch=')?
316
- return '*.rip' if has('-w') or has('--watch')
317
- null
312
+ # Watch mode: on by default, --static disables everything
313
+ watch = getKV('--watch=') or '*.rip'
318
314
 
319
315
  httpsPort = do ->
320
316
  kv = getKV('--https-port=')
@@ -345,7 +341,7 @@ parseFlags = (argv) ->
345
341
  readTimeoutMs: coerceInt(getKV('--read-timeout-ms='), coerceInt(process.env.RIP_READ_TIMEOUT_MS, 30000))
346
342
  jsonLogging: has('--json-logging')
347
343
  accessLog: not has('--no-access-log')
348
- watchGlob: watchGlob
344
+ watch: watch
349
345
  }
350
346
 
351
347
  # ==============================================================================
@@ -401,7 +397,7 @@ runWorker = ->
401
397
 
402
398
  unless h
403
399
  try
404
- api = import!('@rip-lang/api')
400
+ api = import!('@rip-lang/server')
405
401
  h = api?.startHandler?.()
406
402
  catch
407
403
  null
@@ -544,11 +540,11 @@ class Manager
544
540
  @rollingRestart().finally => @isRolling = false
545
541
  , 50
546
542
 
547
- # Watch files in app directory (opt-in via -w/--watch)
548
- if @flags.watchGlob
543
+ # Watch files in app directory (on by default, --static disables)
544
+ if @flags.watch
549
545
  entryFile = @flags.appEntry
550
546
  entryBase = basename(entryFile)
551
- watchExt = if @flags.watchGlob.startsWith('*.') then @flags.watchGlob.slice(1) else null
547
+ watchExt = if @flags.watch.startsWith('*.') then @flags.watch.slice(1) else null
552
548
  debounceMs = @flags.debounce or 250
553
549
  try
554
550
  watch @flags.appBaseDir, { recursive: true }, (event, filename) =>
@@ -556,7 +552,7 @@ class Manager
556
552
  if watchExt
557
553
  return unless filename.endsWith(watchExt)
558
554
  else
559
- return unless filename is @flags.watchGlob or filename.endsWith("/#{@flags.watchGlob}")
555
+ return unless filename is @flags.watch or filename.endsWith("/#{@flags.watch}")
560
556
  return if filename is entryBase or filename.endsWith("/#{entryBase}")
561
557
  # Touch entry file to trigger rolling restart
562
558
  try
@@ -595,7 +591,7 @@ class Manager
595
591
  broadcast('css')
596
592
  watchers.push(w)
597
593
  catch e
598
- console.warn "rip-server: watch failed for #{dir}: #{e.message}"
594
+ console.warn "rip-server: watch failed for #{dir}:\n #{e.message}"
599
595
  @appWatchers.set prefix, { watchers, timer }
600
596
 
601
597
  spawnWorker: (version) ->
@@ -747,38 +743,29 @@ class Server
747
743
  httpOnly = @flags.httpsPort is null
748
744
  fetchFn = @fetch.bind(@)
749
745
 
750
- # Helper to start server, incrementing port if needed
746
+ # Helper to start server, trying the given port first, then incrementing.
747
+ # Falls back to unprivileged range (3000 for HTTP, 3443 for HTTPS) if
748
+ # the initial port fails due to permission denied (EACCES).
751
749
  startOnPort = (p, opts = {}) =>
752
750
  port = p
753
751
  while port < p + 100
754
752
  try
755
753
  return Bun.serve(Object.assign({ port, idleTimeout: 0, fetch: fetchFn }, opts))
756
754
  catch e
755
+ if e?.code is 'EACCES' and port < 1024
756
+ port = if opts.tls then 3443 else 3000
757
+ p = port
758
+ continue
757
759
  throw e unless e?.code is 'EADDRINUSE'
758
760
  port++
759
761
  throw new Error "No available port found (tried #{p}–#{p + 99})"
760
762
 
761
763
  if httpOnly
762
- if @flags.httpPort is 0
763
- try
764
- @server = Bun.serve({ port: 80, idleTimeout: 0, fetch: fetchFn })
765
- catch e
766
- throw e unless e?.code in ['EADDRINUSE', 'EACCES']
767
- @server = startOnPort(5700)
768
- else
769
- @server = startOnPort(@flags.httpPort)
764
+ @server = startOnPort(@flags.httpPort or 80)
770
765
  @flags.httpPort = @server.port
771
766
  else
772
767
  tls = @loadTlsMaterial!
773
-
774
- if not @flags.httpsPort or @flags.httpsPort is 0
775
- try
776
- @httpsServer = Bun.serve({ port: 443, idleTimeout: 0, tls, fetch: fetchFn })
777
- catch e
778
- throw e unless e?.code in ['EADDRINUSE', 'EACCES']
779
- @httpsServer = startOnPort(5700, { tls })
780
- else
781
- @httpsServer = startOnPort(@flags.httpsPort, { tls })
768
+ @httpsServer = startOnPort(@flags.httpsPort or 443, { tls })
782
769
 
783
770
  httpsPort = @httpsServer.port
784
771
  @flags.httpsPort = httpsPort
@@ -1189,15 +1176,14 @@ main = ->
1189
1176
  rip-server - Pure Rip application server
1190
1177
 
1191
1178
  Usage:
1192
- rip-server [options] [app-path] [app-name]
1193
- rip-server [options] [app-path]@<alias1>,<alias2>,...
1179
+ rip serve [options] [app-path] [app-name]
1180
+ rip serve [options] [app-path]@<alias1>,<alias2>,...
1194
1181
 
1195
1182
  Options:
1196
1183
  -h, --help Show this help
1197
1184
  -v, --version Show version
1198
- -w, --watch Watch *.rip files for changes (hot reload)
1199
- -w=<glob> Watch custom glob pattern (e.g., -w=*.ts)
1200
- --static Disable hot reload
1185
+ --watch=<glob> Watch glob pattern (default: *.rip)
1186
+ --static Disable hot reload and file watching
1201
1187
  --env=<mode> Set environment (dev, production)
1202
1188
  --debug Enable debug logging
1203
1189
 
@@ -1217,10 +1203,10 @@ main = ->
1217
1203
  r:<n>,<s>s Restart policy: max requests, max seconds
1218
1204
 
1219
1205
  Examples:
1220
- rip-server Start with ./index.rip
1221
- rip-server -w Start with file watching
1222
- rip-server myapp Start with app name "myapp"
1223
- rip-server api -w 8080 Watch mode on port 8080
1206
+ rip serve Start with ./index.rip (watches *.rip)
1207
+ rip serve myapp Start with app name "myapp"
1208
+ rip serve http HTTP only (no TLS)
1209
+ rip serve --static w:8 Production: no reload, 8 workers
1224
1210
  """
1225
1211
  return
1226
1212
 
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env rip
2
+
3
+ # ==============================================================================
4
+ # Comprehensive read() tests — validates all 37 built-in validators
5
+ # ==============================================================================
6
+ #
7
+ # Each test is self-contained: t(name, input, expected, fn)
8
+ # No HTTP server needed — uses requestContext.run() directly.
9
+ #
10
+ # Run: rip packages/server/tests/read.test.rip
11
+ #
12
+
13
+ import { read, requestContext } from '../api.rip'
14
+
15
+ # ==============================================================================
16
+ # Test harness
17
+ # ==============================================================================
18
+
19
+ stringify = (v) ->
20
+ try JSON.stringify(v) catch then String(v)
21
+
22
+ deepEq = (a, b) -> stringify(a) is stringify(b)
23
+
24
+ passed = 0
25
+ failed = 0
26
+ total = 0
27
+
28
+ # t(name, was, now, fn) — test read() with input 'was', expect 'now'
29
+ t = (name, was, now, fn) ->
30
+ total++
31
+ requestContext.run { data: { v: was } }, ->
32
+ try
33
+ actual = fn()
34
+ if deepEq(actual, now)
35
+ passed++
36
+ console.log "✓ #{name}"
37
+ else
38
+ failed++
39
+ console.log "✗ #{name}"
40
+ console.log " was: #{stringify(was)}"
41
+ console.log " expected: #{stringify(now)}"
42
+ console.log " actual: #{stringify(actual)}"
43
+ catch err
44
+ failed++
45
+ console.log "✗ #{name} — threw: #{err.message}"
46
+
47
+ # f(name, was, fn) — expect a throw
48
+ f = (name, was, fn) ->
49
+ total++
50
+ requestContext.run { data: { v: was } }, ->
51
+ try
52
+ fn()
53
+ failed++
54
+ console.log "✗ #{name} — expected throw"
55
+ catch
56
+ passed++
57
+ console.log "✓ #{name}"
58
+
59
+ # ==============================================================================
60
+ # read() basics (no validator)
61
+ # ==============================================================================
62
+
63
+ t "raw trims strings" , ' hi ' , 'hi' -> read 'v'
64
+ t "raw trims only edges" , ' ab cd ', 'ab cd' -> read 'v'
65
+ t "raw keeps numbers" , 42 , 42 -> read 'v'
66
+ t "raw missing -> null" , undefined , null -> read 'v'
67
+ t "raw uses default" , undefined , 'x' -> read 'v', null, 'x'
68
+ t "raw uses miss fn" , undefined , 'm' -> read 'v', null, -> 'm'
69
+
70
+ # ==============================================================================
71
+ # required (!) behavior
72
+ # ==============================================================================
73
+
74
+ f "required throws" , undefined -> read 'v', 'string!'
75
+ t "required miss fn" , undefined, 'fallback' -> read 'v', 'string!' -> 'fallback'
76
+ f "required blank" , ' ' -> read 'v', 'string!'
77
+
78
+ # ==============================================================================
79
+ # numbers
80
+ # ==============================================================================
81
+
82
+ t "id valid" , '123' , 123 -> read 'v', 'id'
83
+ t "id rejects 0-lead" , '012' , null -> read 'v', 'id'
84
+ t "whole accepts 0" , '0' , 0 -> read 'v', 'whole'
85
+ t "whole rejects neg" , '-5' , null -> read 'v', 'whole'
86
+ t "int positive" , '42' , 42 -> read 'v', 'int'
87
+ t "int negative" , '-5' , -5 -> read 'v', 'int'
88
+ t "int zero" , '0' , 0 -> read 'v', 'int'
89
+ t "int rejects leading 0" , '07' , null -> read 'v', 'int'
90
+ t "int rejects float" , '1.5' , null -> read 'v', 'int'
91
+ t "float signed" , '-1.25', -1.25 -> read 'v', 'float'
92
+ t "float dot" , '.5' , 0.5 -> read 'v', 'float'
93
+ t "float junk" , 'abc' , null -> read 'v', 'float'
94
+
95
+ # ==============================================================================
96
+ # money
97
+ # ==============================================================================
98
+
99
+ t "money basic" , '1.23' , 1.23 -> read 'v', 'money'
100
+ t "money rounds up" , '1.245' , 1.25 -> read 'v', 'money'
101
+ t "money neg half" , '-0.005' , -0.01 -> read 'v', 'money'
102
+ t "money strips $," , '$7,221.245' , 7221.25 -> read 'v', 'money'
103
+ t "money spaced" , '- $ 9,999.12', -9999.12 -> read 'v', 'money'
104
+ t "money_even basic" , '1.23' , 1.23 -> read 'v', 'money_even'
105
+ t "money_even rounds even" , '1.245' , 1.24 -> read 'v', 'money_even'
106
+ t "money_even neg half" , '-0.005' , 0 -> read 'v', 'money_even'
107
+ t "cents basic" , '1.23' , 123 -> read 'v', 'cents'
108
+ t "cents rounds up" , '1.245' , 125 -> read 'v', 'cents'
109
+ t "cents neg half" , '-0.005' , -1 -> read 'v', 'cents'
110
+ t "cents past half" , '1.2351' , 124 -> read 'v', 'cents'
111
+ t "cents strips $," , '$7,221.245' , 722125 -> read 'v', 'cents'
112
+ t "cents_even basic" , '1.23' , 123 -> read 'v', 'cents_even'
113
+ t "cents_even rounds even" , '1.245' , 124 -> read 'v', 'cents_even'
114
+ t "cents_even past half" , '1.2351' , 124 -> read 'v', 'cents_even'
115
+ t "cents_even neg half" , '-0.005' , 0 -> read 'v', 'cents_even'
116
+
117
+ # ==============================================================================
118
+ # formatting
119
+ # ==============================================================================
120
+
121
+ t "string collapses" , ' ab cd ' , 'ab cd' -> read 'v', 'string'
122
+ t "text collapses" , 'a b' , 'a b' -> read 'v', 'text'
123
+ t "name normalizes" , "al\t\nbeck" , 'Al Beck' -> read 'v', 'name'
124
+ t "name is magical" , " lArrY o'dOUl", "Larry O'Doul" -> read 'v', 'name'
125
+ t "address normalizes" , "1\t Main St", '1 Main St' -> read 'v', 'address'
126
+ t "address PO Box" , 'P. o. bOX #44', 'PO Box #44' -> read 'v', 'address'
127
+
128
+ # ==============================================================================
129
+ # time/date
130
+ # ==============================================================================
131
+
132
+ t "time HH:MM" , '09:30' , '09:30' -> read 'v', 'time'
133
+ t "time HH:MM:SS" , '23:59:59' , '23:59:59' -> read 'v', 'time'
134
+ t "time rejects 24" , '24:00' , null -> read 'v', 'time'
135
+ t "date ok" , '2026-01-18' , '2026-01-18' -> read 'v', 'date'
136
+ t "date rejects bad" , '2026-1-8' , null -> read 'v', 'date'
137
+ t "time ok" , '0:01' , '0:01' -> read 'v', 'time'
138
+ t "time 24 hr" , '16:05:30' , '16:05:30' -> read 'v', 'time'
139
+ t "time12 lowercased" , '9:05 PM' , '9:05 pm' -> read 'v', 'time12'
140
+
141
+ # ==============================================================================
142
+ # booleans
143
+ # ==============================================================================
144
+
145
+ t "truthy yes" , 'yes' , true -> read 'v', 'truthy'
146
+ t "truthy non-match" , 'maybe', null -> read 'v', 'truthy'
147
+ t "falsy no" , 'no' , true -> read 'v', 'falsy'
148
+ t "bool true" , 'ON' , true -> read 'v', 'bool'
149
+ t "bool false" , 'off' , false -> read 'v', 'bool'
150
+ t "bool junk" , 'wat' , null -> read 'v', 'bool'
151
+
152
+ # ==============================================================================
153
+ # identity/contact
154
+ # ==============================================================================
155
+
156
+ t "email ok" , 'a+b@ex.co' , 'a+b@ex.co' -> read 'v', 'email'
157
+ t "email bad" , 'nope@' , null -> read 'v', 'email'
158
+ t "state uppercases" , 'ca' , 'CA' -> read 'v', 'state'
159
+ t "zip reads 5" , '12345' , '12345' -> read 'v', 'zip'
160
+ t "zipplus4 formats" , '12345-6789' , '12345-6789' -> read 'v', 'zipplus4'
161
+ t "ssn strips punct" , '123-45-6789', '123456789' -> read 'v', 'ssn'
162
+ t "sex normalizes" , 'Female' , 'f' -> read 'v', 'sex'
163
+
164
+ # ==============================================================================
165
+ # technical/web
166
+ # ==============================================================================
167
+
168
+ t "username lower" , 'AbC_12',
169
+ 'abc_12' -> read 'v', 'username'
170
+ t "username short" , 'ab',
171
+ null -> read 'v', 'username'
172
+ t "ip valid" , '192.168.1.1',
173
+ '192.168.1.1' -> read 'v', 'ip'
174
+ t "ip rejects 999" , '999.1.2.3',
175
+ null -> read 'v', 'ip'
176
+ t "mac normalizes" , 'AA-BB-CC-DD-EE-FF',
177
+ 'aa:bb:cc:dd:ee:ff' -> read 'v', 'mac'
178
+ t "url ok" , 'https://example.com/a/b',
179
+ 'https://example.com/a/b' -> read 'v', 'url'
180
+ t "color hash" , 'ABC',
181
+ '#abc' -> read 'v', 'color'
182
+ t "uuid normalizes" , 'A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5',
183
+ 'a0b1c2d3-e4f5-a6b7-c8d9-e0f1a2b3c4d5' -> read 'v', 'uuid'
184
+ t "semver ok" , '1.2.3-alpha.1+build.5',
185
+ '1.2.3-alpha.1+build.5' -> read 'v', 'semver'
186
+ t "slug ok" , 'a-b-c',
187
+ 'a-b-c' -> read 'v', 'slug'
188
+ t "slug capitals" , 'A-B',
189
+ 'a-b' -> read 'v', 'slug'
190
+ t "phone formats" , '555234-5678',
191
+ '(555) 234-5678' -> read 'v', 'phone'
192
+ t "phone short" , '123',
193
+ null -> read 'v', 'phone'
194
+
195
+ # ==============================================================================
196
+ # collections
197
+ # ==============================================================================
198
+
199
+ t "array ok" , [1, 2] , [1, 2] -> read 'v', 'array'
200
+ t "array not object" , { x: 1 } , null -> read 'v', 'array'
201
+ t "hash ok" , { x: 1 } , { x: 1 } -> read 'v', 'hash'
202
+ t "hash not array" , [] , null -> read 'v', 'hash'
203
+ t "json parses str" , '{"a":1}' , { a: 1 } -> read 'v', 'json'
204
+ t "json accepts obj" , { a: 1 } , { a: 1 } -> read 'v', 'json'
205
+ t "json rejects junk" , '{' , null -> read 'v', 'json'
206
+ t "ids sorts dedupes" , '3, 2 2 1', [1, 2, 3] -> read 'v', 'ids'
207
+ t "ids rejects 0" , '0 1' , null -> read 'v', 'ids'
208
+ t "ids empty null" , ' ' , null -> read 'v', 'ids'
209
+
210
+ # ==============================================================================
211
+ # regex type
212
+ # ==============================================================================
213
+
214
+ t "regex matches" , 'abc' , 'abc' -> read 'v', /^[a-z]+$/
215
+ t "regex non-match" , 'ABC' , null -> read 'v', /^[a-z]+$/
216
+
217
+ # ==============================================================================
218
+ # array range (numeric + string length + enum)
219
+ # ==============================================================================
220
+
221
+ t "range num ok str" , '2' , 2 -> read 'v', [1, 3]
222
+ t "range num ok val" , 3 , 3 -> read 'v', [1, 3]
223
+ t "range num low" , '0' , null -> read 'v', [1, 3]
224
+ t "range str ok" , 'abcd' , 'abcd' -> read 'v', [2, 4]
225
+ t "range str long" , 'abcde' , null -> read 'v', [2, 4]
226
+ t "enum ok" , 'green' , 'green' -> read 'v', ['red', 'green']
227
+ t "enum bad" , 'blue' , null -> read 'v', ['red', 'green']
228
+
229
+ # ==============================================================================
230
+ # object range (start/end + min/max)
231
+ # ==============================================================================
232
+
233
+ t "obj start/end ok" , '7' , 7 -> read 'v', { start: 5, end: 10 }
234
+ t "obj start/end dec" , '1.5' , null -> read 'v', { start: 0, end: 10 }
235
+ t "obj min/max ok" , '2' , 2 -> read 'v', { min: 1, max: 3 }
236
+ t "obj min/max bad" , '9' , null -> read 'v', { min: 1, max: 3 }
237
+ t "obj str ok" , 'abc' , 'abc' -> read 'v', { min: 2, max: 4 }
238
+ t "obj str short" , 'a' , null -> read 'v', { min: 2, max: 4 }
239
+
240
+ # ==============================================================================
241
+ # miss() edge cases
242
+ # ==============================================================================
243
+
244
+ t "miss uses default" , undefined, 'no' -> read 'v', 'email', 'no'
245
+ t "miss uses fn" , undefined, 'no' -> read 'v', 'email', -> 'no'
246
+ f "required w/ default" , undefined -> read 'v', 'email!', 'nope'
247
+ t "required w/ miss()" , undefined, 'ok' -> read 'v', 'email!', -> 'ok'
248
+
249
+ # ==============================================================================
250
+ # Results
251
+ # ==============================================================================
252
+
253
+ console.log "\n#{passed} passed, #{failed} failed (#{total} total)"
254
+ process.exit(if failed > 0 then 1 else 0)