@rip-lang/server 1.3.77 → 1.3.79

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.
Files changed (3) hide show
  1. package/default.rip +248 -0
  2. package/package.json +2 -1
  3. package/server.rip +10 -6
package/default.rip ADDED
@@ -0,0 +1,248 @@
1
+ # ==============================================================================
2
+ # @rip-lang/server — Default Static File Server
3
+ # ==============================================================================
4
+ #
5
+ # Built-in fallback when no index.rip or index.ts exists in the target directory.
6
+ # Activated automatically by `rip server` when no app entry is found.
7
+ #
8
+ # Behavior:
9
+ # - Serves static files with auto-detected MIME types
10
+ # - Directories with index.html serve the index.html
11
+ # - Directories without index.html show a browsable file listing
12
+ # - Rip browser bundle available at /rip/rip.min.js
13
+ # - Hot-reload via SSE for .rip, .html, and .css changes
14
+ #
15
+ # ==============================================================================
16
+
17
+ import { use, start, notFound } from '@rip-lang/server'
18
+ import { serve } from '@rip-lang/server/middleware'
19
+ import { statSync, readdirSync } from 'node:fs'
20
+ import { join, resolve, basename } from 'node:path'
21
+
22
+ root = resolve(process.env.APP_BASE_DIR or process.cwd())
23
+ rootSlash = root + '/'
24
+ rootName = basename(root) or root
25
+
26
+ use serve dir: root, bundle: [], watch: true
27
+
28
+ # --- Helpers ---
29
+
30
+ esc = (s) ->
31
+ s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
32
+
33
+ fmtSize = (n) ->
34
+ for u in ['B', 'KB', 'MB', 'GB']
35
+ if n < 1024
36
+ return if u is 'B' then "#{n} B" else "#{n.toFixed(1)} #{u}"
37
+ n /= 1024
38
+ "#{n.toFixed(1)} TB"
39
+
40
+ fmtDate = (ms) ->
41
+ d = new Date(ms)
42
+ mo = d.toLocaleString('en', { month: 'short' })
43
+ day = String(d.getDate()).padStart(2, '0')
44
+ year = d.getFullYear()
45
+ hour = d.getHours()
46
+ min = String(d.getMinutes()).padStart(2, '0')
47
+ ampm = if hour >= 12 then 'PM' else 'AM'
48
+ hour = hour % 12 or 12
49
+ "#{mo} #{day}, #{year} at #{hour}:#{min} #{ampm}"
50
+
51
+ EXT_ICONS =
52
+ html: '🌐', htm: '🌐', css: '🎨', js: '📜', mjs: '📜', ts: '📜'
53
+ json: '📋', rip: '⚡', md: '📝', txt: '📝', pdf: '📕'
54
+ png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', svg: '🖼️', webp: '🖼️', ico: '🖼️'
55
+ zip: '📦', gz: '📦', tar: '📦'
56
+ mp3: '🎵', ogg: '🎵', wav: '🎵', mp4: '🎬', webm: '🎬'
57
+ woff: '🔤', woff2: '🔤', ttf: '🔤', otf: '🔤'
58
+
59
+ fileIcon = (name) ->
60
+ EXT_ICONS[name.slice(name.lastIndexOf('.') + 1).toLowerCase()] or '📄'
61
+
62
+ row = (ic, href, label, size = '', date = '') ->
63
+ "<tr><td class=\"ic\">#{ic}</td><td><a href=\"#{esc href}\">#{esc label}</a></td><td class=\"sz\">#{size}</td><td class=\"dt\">#{date}</td></tr>"
64
+
65
+ breadcrumbs = (reqPath) ->
66
+ parts = reqPath.split('/').filter((p) -> p)
67
+ crumbs = "<a href=\"/\">#{esc rootName}</a>"
68
+ href = ''
69
+ for part in parts
70
+ href += "/#{part}"
71
+ crumbs += " <span class=\"sep\">/</span> <a href=\"#{href}/\">#{esc part}</a>"
72
+ crumbs
73
+
74
+ # --- Directory Index ---
75
+
76
+ renderIndex = (reqPath, fsPath) ->
77
+ entries = try readdirSync(fsPath, { withFileTypes: true }) catch then []
78
+ dirs = []
79
+ files = []
80
+ for e in entries
81
+ continue if e.name.startsWith('.')
82
+ if e.isDirectory() then dirs.push(e.name) else files.push(e.name)
83
+ dirs.sort()
84
+ files.sort()
85
+
86
+ rows = []
87
+ rows.push row('📁', '../', '..') if reqPath isnt '/'
88
+ for d in dirs
89
+ rows.push row('📁', "#{d}/", d)
90
+ for f in files
91
+ try
92
+ s = statSync(join(fsPath, f))
93
+ rows.push row(fileIcon(f), f, f, fmtSize(s.size), fmtDate(s.mtimeMs))
94
+ catch
95
+ rows.push row(fileIcon(f), f, f)
96
+
97
+ numDirs = dirs.length
98
+ numFiles = files.length
99
+ summary = []
100
+ summary.push "#{numDirs} folder#{if numDirs isnt 1 then 's' else ''}" if numDirs > 0
101
+ summary.push "#{numFiles} file#{if numFiles isnt 1 then 's' else ''}" if numFiles > 0
102
+
103
+ """
104
+ <!DOCTYPE html>
105
+ <html lang="en">
106
+ <head>
107
+ <meta charset="UTF-8">
108
+ <meta name="viewport" content="width=device-width, initial-scale=1">
109
+ <title>Index of #{esc reqPath}</title>
110
+ <style>
111
+ :root {
112
+ --bg: #f3f6f7;
113
+ --card: #fff;
114
+ --border: #e5e7eb;
115
+ --border-row: #f0f0f0;
116
+ --text: #1f2937;
117
+ --muted: #6b7280;
118
+ --link: #2563eb;
119
+ --link-visited: #7c3aed;
120
+ --hover-bg: #f0f7ff;
121
+ --head-bg: #fafbfc;
122
+ --sep: #cbd5e1;
123
+ }
124
+ @media (prefers-color-scheme: dark) {
125
+ :root {
126
+ --bg: #0d1117;
127
+ --card: #161b22;
128
+ --border: #30363d;
129
+ --border-row: #21262d;
130
+ --text: #c9d1d9;
131
+ --muted: #8b949e;
132
+ --link: #58a6ff;
133
+ --link-visited: #bc8cff;
134
+ --hover-bg: #1c2129;
135
+ --head-bg: #1c2129;
136
+ --sep: #484f58;
137
+ }
138
+ }
139
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
140
+ body {
141
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
142
+ font-size: 15px;
143
+ line-height: 1.5;
144
+ color: var(--text);
145
+ background: var(--bg);
146
+ -webkit-font-smoothing: antialiased;
147
+ }
148
+ .wrap { max-width: 960px; margin: 2.5rem auto; padding: 0 1rem; }
149
+ .card {
150
+ background: var(--card);
151
+ border: 1px solid var(--border);
152
+ border-radius: 8px;
153
+ overflow: hidden;
154
+ box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
155
+ }
156
+ .bar {
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ padding: .875rem 1.25rem;
161
+ border-bottom: 1px solid var(--border);
162
+ gap: 1rem;
163
+ }
164
+ .crumbs { font-size: .9375rem; font-weight: 500; }
165
+ .crumbs a { color: var(--link); text-decoration: none; }
166
+ .crumbs a:hover { text-decoration: underline; }
167
+ .sep { color: var(--sep); margin: 0 .3rem; font-weight: 400; }
168
+ .meta { font-size: .8125rem; color: var(--muted); white-space: nowrap; }
169
+ table { width: 100%; border-collapse: collapse; }
170
+ th {
171
+ position: sticky; top: 0;
172
+ background: var(--head-bg);
173
+ text-align: left;
174
+ padding: .625rem 1.25rem;
175
+ font-size: .6875rem;
176
+ font-weight: 600;
177
+ color: var(--muted);
178
+ text-transform: uppercase;
179
+ letter-spacing: .05em;
180
+ border-bottom: 1px solid var(--border);
181
+ }
182
+ td {
183
+ padding: .5rem 1.25rem;
184
+ border-bottom: 1px solid var(--border-row);
185
+ font-size: .875rem;
186
+ vertical-align: middle;
187
+ }
188
+ tr:last-child td { border-bottom: none; }
189
+ tbody tr:hover { background: var(--hover-bg); }
190
+ td a { color: var(--link); text-decoration: none; }
191
+ td a:hover { color: var(--link); text-decoration: underline; }
192
+ td a:visited { color: var(--link-visited); }
193
+ .ic { width: 2.5rem; text-align: center; font-size: 1.125rem; }
194
+ .sz { width: 6rem; text-align: right; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
195
+ .dt { color: var(--muted); white-space: nowrap; }
196
+ th.sz { text-align: right; }
197
+ @media (max-width: 640px) {
198
+ .wrap { margin: 1rem auto; }
199
+ td, th { padding: .5rem .75rem; }
200
+ .dt, th.dt { display: none; }
201
+ .bar { padding: .75rem 1rem; }
202
+ }
203
+ </style>
204
+ </head>
205
+ <body>
206
+ <div class="wrap">
207
+ <div class="card">
208
+ <div class="bar">
209
+ <div class="crumbs">#{breadcrumbs reqPath}</div>
210
+ <div class="meta">#{summary.join(' &middot; ')}</div>
211
+ </div>
212
+ <table>
213
+ <thead><tr><th></th><th>Name</th><th class="sz">Size</th><th class="dt">Modified</th></tr></thead>
214
+ <tbody>
215
+ #{rows.join('\n')}
216
+ </tbody>
217
+ </table>
218
+ </div>
219
+ </div>
220
+ </body>
221
+ </html>
222
+ """
223
+
224
+ # --- Request Handler ---
225
+
226
+ notFound ->
227
+ reqPath = @req.path
228
+ path = resolve(root, reqPath.slice(1))
229
+
230
+ unless path is root or path.startsWith(rootSlash)
231
+ return new Response 'Not Found', { status: 404 }
232
+
233
+ try
234
+ stat = statSync(path)
235
+ catch
236
+ return new Response 'Not Found', { status: 404 }
237
+
238
+ if stat.isFile()
239
+ return @send path
240
+
241
+ if stat.isDirectory()
242
+ return new Response(null, { status: 301, headers: { Location: "#{reqPath}/" } }) unless reqPath.endsWith('/')
243
+ try return @send join(path, 'index.html'), 'text/html; charset=UTF-8' if statSync(join(path, 'index.html')).isFile()
244
+ return new Response renderIndex(reqPath, path), { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
245
+
246
+ new Response 'Not Found', { status: 404 }
247
+
248
+ start()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/server",
3
- "version": "1.3.77",
3
+ "version": "1.3.79",
4
4
  "description": "Pure Rip web framework and application server",
5
5
  "type": "module",
6
6
  "main": "api.rip",
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "files": [
51
51
  "api.rip",
52
+ "default.rip",
52
53
  "middleware.rip",
53
54
  "server.rip",
54
55
  "server.html",
package/server.rip CHANGED
@@ -165,6 +165,8 @@ parseRestartPolicy = (token, defReqs, defSecs) ->
165
165
 
166
166
  { maxRequests, maxSeconds }
167
167
 
168
+ defaultEntry = join(import.meta.dir, 'default.rip')
169
+
168
170
  resolveAppEntry = (appPathInput) ->
169
171
  abs = if isAbsolute(appPathInput) then appPathInput else resolve(process.cwd(), appPathInput)
170
172
 
@@ -177,8 +179,8 @@ resolveAppEntry = (appPathInput) ->
177
179
  else if existsSync(two)
178
180
  entryPath = two
179
181
  else
180
- console.error "No app entry found. Probed: #{one}, #{two}"
181
- process.exit(2)
182
+ console.log "rip-server: no index.rip found, serving static files from #{abs}\n Create an index.rip to customize, or run 'rip server --help' for options."
183
+ entryPath = defaultEntry
182
184
  else
183
185
  unless existsSync(abs)
184
186
  console.error "App path not found: #{abs}"
@@ -247,10 +249,7 @@ parseFlags = (argv) ->
247
249
  else if existsSync(indexTs)
248
250
  appPathInput = indexTs
249
251
  else
250
- console.error 'No app entry found. Looked for index.rip or index.ts in current directory.'
251
- console.error 'Usage: rip-server [flags] [app-path] [app-name]'
252
- console.error ' rip-server [flags] [app-path]@<alias1>,<alias2>,...'
253
- process.exit(2)
252
+ appPathInput = cwd
254
253
 
255
254
  getKV = (prefix) ->
256
255
  for f as rawFlags
@@ -620,6 +619,7 @@ class Manager
620
619
  SOCKET_PATH: socketPath
621
620
  SOCKET_PREFIX: @flags.socketPrefix
622
621
  APP_ENTRY: @flags.appEntry
622
+ APP_BASE_DIR: @flags.appBaseDir
623
623
  MAX_REQUESTS: String(@flags.maxRequestsPerWorker)
624
624
  MAX_SECONDS: String(@flags.maxSecondsPerWorker)
625
625
  RIP_LOG_JSON: if @flags.jsonLogging then '1' else '0'
@@ -1212,6 +1212,10 @@ main = ->
1212
1212
  rip serve myapp Start with app name "myapp"
1213
1213
  rip serve http HTTP only (no TLS)
1214
1214
  rip serve --static w:8 Production: no reload, 8 workers
1215
+
1216
+ If no index.rip or index.ts is found, a built-in static file server
1217
+ activates with directory indexes, auto-detected MIME types, and
1218
+ hot-reload. Create an index.rip to customize server behavior.
1215
1219
  """
1216
1220
  return
1217
1221