@rip-lang/ui 0.1.2 → 0.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 +127 -796
- package/package.json +6 -17
- package/serve.rip +70 -70
- package/ui.rip +935 -0
- package/renderer.js +0 -397
- package/router.js +0 -325
- package/stash.js +0 -413
- package/ui.js +0 -208
- package/vfs.js +0 -215
package/README.md
CHANGED
|
@@ -1,877 +1,208 @@
|
|
|
1
|
-
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.
|
|
1
|
+
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" style="width:50px" /> <br>
|
|
2
2
|
|
|
3
3
|
# Rip UI - @rip-lang/ui
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **Zero-build reactive web framework for the Rip language.**
|
|
6
6
|
|
|
7
|
-
Rip
|
|
8
|
-
|
|
9
|
-
40KB Rip compiler itself. Components are delivered as source files, stored in a
|
|
10
|
-
browser-local Virtual File System, compiled on demand, and rendered with
|
|
11
|
-
fine-grained DOM updates powered by Rip's built-in reactivity. No build step.
|
|
12
|
-
No bundler. No configuration files.
|
|
7
|
+
Load the Rip compiler in the browser. Write inline Rip. Launch your app.
|
|
8
|
+
No build step, no bundler, no configuration.
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
`component` and `render` — and reuses everything else (classes, reactivity,
|
|
16
|
-
functions, methods) that Rip already provides.
|
|
17
|
-
|
|
18
|
-
## Architecture
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
┌─────────────────────────────────────────┐
|
|
22
|
-
│ Server (Bun + @rip-lang/api) │
|
|
23
|
-
│ │
|
|
24
|
-
│ serve.rip (ripUI middleware) │
|
|
25
|
-
│ ├── /rip-ui/*.js → framework files │
|
|
26
|
-
│ ├── /rip-ui/manifest.json → all pages │
|
|
27
|
-
│ ├── /rip-ui/watch → SSE hot-reload │
|
|
28
|
-
│ └── /pages/*.rip → individual pages │
|
|
29
|
-
└──────────────┬──────────────────────────┘
|
|
30
|
-
│
|
|
31
|
-
┌──────────────────────────┼──────────────────────────┐
|
|
32
|
-
│ │ │
|
|
33
|
-
▼ ▼ ▼
|
|
34
|
-
rip.browser.js (40KB) @rip-lang/ui (~8KB) SSE EventSource
|
|
35
|
-
(Rip compiler) (framework modules) (hot-reload channel)
|
|
36
|
-
│ │ │
|
|
37
|
-
│ ┌────────────────┼────────────────┐ │
|
|
38
|
-
│ │ │ │ │
|
|
39
|
-
│ Reactive Stash Virtual FS Router │
|
|
40
|
-
│ (app state) (file storage) (URL → VFS) │
|
|
41
|
-
│ │ │ │ │
|
|
42
|
-
│ └────────────────┼────────────────┘ │
|
|
43
|
-
│ │ │
|
|
44
|
-
└─────────────────► Renderer ◄────────────────────┘
|
|
45
|
-
(compiles + mounts)
|
|
46
|
-
│
|
|
47
|
-
DOM
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
| Module | Size | Role |
|
|
51
|
-
|--------|------|------|
|
|
52
|
-
| `ui.js` | ~175 lines | `createApp` entry point with `loadBundle`, `watch`, re-exports |
|
|
53
|
-
| `stash.js` | ~400 lines | Deep reactive state tree with path-based navigation |
|
|
54
|
-
| `vfs.js` | ~200 lines | Browser-local Virtual File System with watchers |
|
|
55
|
-
| `router.js` | ~300 lines | File-based router (URL ↔ VFS paths, History API) |
|
|
56
|
-
| `renderer.js` | ~250 lines | Component lifecycle, layouts, transitions, `remount` |
|
|
57
|
-
| `serve.rip` | ~140 lines | Server middleware: framework files, manifest, SSE hot-reload |
|
|
58
|
-
|
|
59
|
-
## The Idea
|
|
60
|
-
|
|
61
|
-
Modern web frameworks — React, Vue, Svelte, Solid — all share the same
|
|
62
|
-
fundamental assumption: code must be compiled and bundled on the developer's
|
|
63
|
-
machine, then shipped as static artifacts. This creates an entire ecosystem of
|
|
64
|
-
build tools (Vite, Webpack, Turbopack, esbuild), configuration files, dev
|
|
65
|
-
servers, hot module replacement protocols, and deployment pipelines. The
|
|
66
|
-
developer experience is powerful, but the machinery is enormous.
|
|
67
|
-
|
|
68
|
-
Rip UI asks: **what if the compiler ran in the browser?**
|
|
69
|
-
|
|
70
|
-
At 40KB, the Rip compiler is small enough to ship alongside your application.
|
|
71
|
-
Components arrive as `.rip` source files — plain text — and are compiled to
|
|
72
|
-
JavaScript on the client's machine. This eliminates the build step entirely.
|
|
73
|
-
There is no `dist/` folder, no source maps, no chunk splitting, no tree
|
|
74
|
-
shaking, no CI build minutes. You write a `.rip` file, the browser compiles it,
|
|
75
|
-
and it runs.
|
|
76
|
-
|
|
77
|
-
This is not a toy or a limitation. The compiler produces the same output it
|
|
78
|
-
would on a server. The reactive system is the same signal-based engine that
|
|
79
|
-
powers server-side Rip. The component model compiles to anonymous ES6 classes
|
|
80
|
-
with fine-grained DOM manipulation — no virtual DOM diffing.
|
|
81
|
-
|
|
82
|
-
### How It Differs from Existing Frameworks
|
|
83
|
-
|
|
84
|
-
| | React/Vue/Svelte | Rip UI |
|
|
85
|
-
|---|---|---|
|
|
86
|
-
| **Build step** | Required (Vite, Webpack, etc.) | None — compiler runs in browser |
|
|
87
|
-
| **Bundle size** | 40-100KB+ framework + app bundle | 40KB compiler + ~8KB framework + raw source |
|
|
88
|
-
| **HMR** | Dev server ↔ browser WebSocket | SSE notify + VFS invalidation + recompile |
|
|
89
|
-
| **Deployment** | Build artifacts (`dist/`) | Source files served as-is |
|
|
90
|
-
| **Component format** | JSX, SFC, templates | Rip source (`.rip` files) |
|
|
91
|
-
| **Reactivity** | Library-specific (hooks, refs, signals) | Language-native (`:=`, `~=`, `~>`) |
|
|
92
|
-
| **DOM updates** | Virtual DOM diff or compiled transforms | Fine-grained effects, direct DOM mutation |
|
|
93
|
-
| **Routing** | Framework plugin (react-router, etc.) | Built-in file-based router over VFS |
|
|
94
|
-
| **State management** | External library (Redux, Pinia, etc.) | Built-in reactive stash with deep tracking |
|
|
95
|
-
|
|
96
|
-
## Component Model
|
|
97
|
-
|
|
98
|
-
The component system adds two keywords to Rip: `component` and `render`. Everything
|
|
99
|
-
else — reactive state (`:=`), computed values (`~=`), effects (`~>`), methods,
|
|
100
|
-
lifecycle — uses standard Rip syntax. A component is an expression that evaluates
|
|
101
|
-
to an anonymous ES6 class.
|
|
102
|
-
|
|
103
|
-
### Defining a Component
|
|
104
|
-
|
|
105
|
-
```coffee
|
|
106
|
-
Counter = component
|
|
107
|
-
@count := 0 # reactive state (signal) — parent can override via props
|
|
108
|
-
@step = 1 # plain prop — parent can set, not reactive
|
|
109
|
-
|
|
110
|
-
doubled ~= @count * 2 # computed (derived, read-only)
|
|
111
|
-
|
|
112
|
-
increment: -> @count += @step
|
|
113
|
-
decrement: -> @count -= @step
|
|
114
|
-
|
|
115
|
-
mounted: ->
|
|
116
|
-
console.log "Counter mounted"
|
|
117
|
-
|
|
118
|
-
render
|
|
119
|
-
div.counter
|
|
120
|
-
h1 "Count: #{@count}"
|
|
121
|
-
p "Doubled: #{doubled}"
|
|
122
|
-
button @click: @increment, "+#{@step}"
|
|
123
|
-
button @click: @decrement, "-#{@step}"
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
This compiles to an anonymous ES6 class expression:
|
|
127
|
-
|
|
128
|
-
```javascript
|
|
129
|
-
Counter = class {
|
|
130
|
-
constructor(props = {}) {
|
|
131
|
-
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
132
|
-
this.step = props.step ?? 1;
|
|
133
|
-
this.doubled = __computed(() => this.count.value * 2);
|
|
134
|
-
}
|
|
135
|
-
increment() { return this.count.value += this.step; }
|
|
136
|
-
decrement() { return this.count.value -= this.step; }
|
|
137
|
-
mounted() { return console.log("Counter mounted"); }
|
|
138
|
-
_create() { /* fine-grained DOM creation */ }
|
|
139
|
-
_setup() { /* reactive effect bindings */ }
|
|
140
|
-
mount(target) { /* ... */ }
|
|
141
|
-
unmount() { /* ... */ }
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### The Two Keywords
|
|
146
|
-
|
|
147
|
-
**`component`** — Declares an anonymous class with component semantics:
|
|
148
|
-
|
|
149
|
-
- `@` properties become instance variables that the parent can set via props
|
|
150
|
-
- `:=` assignments create reactive signals (`__state`)
|
|
151
|
-
- `~=` assignments create computed values (`__computed`)
|
|
152
|
-
- `~>` assignments create effects (`__effect`)
|
|
153
|
-
- Plain `=` assignments with function values become methods
|
|
154
|
-
- `mounted`, `unmounted`, `updated` are lifecycle hooks called by the runtime
|
|
155
|
-
- Everything else is standard Rip class behavior
|
|
156
|
-
|
|
157
|
-
**`render`** — Defines the component's template using a Pug/Jade-like DSL:
|
|
158
|
-
|
|
159
|
-
- Tags are bare identifiers: `div`, `h1`, `button`, `span`
|
|
160
|
-
- Classes use dot notation: `div.card.active`, `button.btn.btn-primary`
|
|
161
|
-
- Dynamic classes use dot-parens: `div.("active" if @selected)` (CLSX-like)
|
|
162
|
-
- Attributes use object syntax: `input type: "text", placeholder: "..."`
|
|
163
|
-
- Events use `@` prefix: `button @click: @handleClick`
|
|
164
|
-
- Text content is a string argument: `h1 "Hello"`
|
|
165
|
-
- Interpolation works: `p "Count: #{@count}"`
|
|
166
|
-
- Children are indented below their parent
|
|
167
|
-
- `if`/`else` and `for...in` work inside templates
|
|
168
|
-
|
|
169
|
-
That's it. No special attribute syntax, no directive system, no template
|
|
170
|
-
compiler — just Rip's existing syntax applied to DOM construction.
|
|
171
|
-
|
|
172
|
-
### Props
|
|
173
|
-
|
|
174
|
-
Every `@` property on a component is a prop that the parent can set. The child
|
|
175
|
-
owns the property; the parent can provide an initial value or pass a reactive
|
|
176
|
-
signal:
|
|
177
|
-
|
|
178
|
-
```coffee
|
|
179
|
-
# Parent passes plain value — child wraps it in a signal
|
|
180
|
-
Counter {count: 10}
|
|
181
|
-
|
|
182
|
-
# Parent passes its own signal — child uses it directly (shared state)
|
|
183
|
-
Counter {count: parentCount}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
The `isSignal` check in the constructor handles this automatically:
|
|
187
|
-
|
|
188
|
-
```javascript
|
|
189
|
-
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
If you don't want the parent to override a prop, don't use `@`:
|
|
193
|
-
|
|
194
|
-
```coffee
|
|
195
|
-
MyComponent = component
|
|
196
|
-
active := false # internal state — not a prop, parent can't set it
|
|
197
|
-
@count := 0 # prop — parent can set or share a signal
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Required and Optional Props
|
|
201
|
-
|
|
202
|
-
```coffee
|
|
203
|
-
UserCard = component
|
|
204
|
-
@name # required — no default, error if missing
|
|
205
|
-
@avatar = "/default.png" # optional — has a default
|
|
206
|
-
@bio? := "" # optional reactive — ? makes it explicitly optional
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### Computed Values and Effects
|
|
210
|
-
|
|
211
|
-
```coffee
|
|
212
|
-
TodoList = component
|
|
213
|
-
@todos := []
|
|
214
|
-
remaining ~= @todos.filter((t) -> not t.done).length
|
|
215
|
-
total ~= @todos.length
|
|
216
|
-
|
|
217
|
-
~> console.log "#{remaining} of #{total} remaining"
|
|
218
|
-
|
|
219
|
-
render
|
|
220
|
-
div
|
|
221
|
-
h2 "Todos (#{remaining}/#{total})"
|
|
222
|
-
# ...
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Lifecycle
|
|
226
|
-
|
|
227
|
-
`mounted`, `unmounted`, and `updated` are just methods. No special syntax. The
|
|
228
|
-
runtime calls them at the appropriate times:
|
|
229
|
-
|
|
230
|
-
```coffee
|
|
231
|
-
Timer = component
|
|
232
|
-
@elapsed := 0
|
|
233
|
-
@interval = null
|
|
234
|
-
|
|
235
|
-
mounted: ->
|
|
236
|
-
@interval = setInterval (=> @elapsed += 1), 1000
|
|
237
|
-
|
|
238
|
-
unmounted: ->
|
|
239
|
-
clearInterval @interval
|
|
240
|
-
|
|
241
|
-
render
|
|
242
|
-
p "#{@elapsed} seconds"
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Two-Way Binding
|
|
246
|
-
|
|
247
|
-
The `<=>` operator creates two-way bindings between form elements and reactive
|
|
248
|
-
state:
|
|
249
|
-
|
|
250
|
-
```coffee
|
|
251
|
-
SearchBox = component
|
|
252
|
-
@query := ""
|
|
253
|
-
|
|
254
|
-
render
|
|
255
|
-
input type: "text", value <=> @query
|
|
256
|
-
p "Searching for: #{@query}"
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Child Components
|
|
10
|
+
## Quick Start
|
|
260
11
|
|
|
261
|
-
|
|
12
|
+
**`index.rip`** — the server:
|
|
262
13
|
|
|
263
14
|
```coffee
|
|
264
|
-
|
|
265
|
-
|
|
15
|
+
import { get, use, start, notFound } from '@rip-lang/api'
|
|
16
|
+
import { ripUI } from '@rip-lang/ui/serve'
|
|
266
17
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
18
|
+
dir = import.meta.dir
|
|
19
|
+
use ripUI dir: dir, watch: true, title: 'My App'
|
|
20
|
+
get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
|
|
21
|
+
notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
|
|
22
|
+
start port: 3000
|
|
272
23
|
```
|
|
273
24
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
Components can share state down the tree without passing props at every level:
|
|
277
|
-
|
|
278
|
-
```coffee
|
|
279
|
-
# In a parent component's constructor:
|
|
280
|
-
setContext 'theme', @theme
|
|
25
|
+
**`index.html`** — the page:
|
|
281
26
|
|
|
282
|
-
|
|
283
|
-
|
|
27
|
+
```html
|
|
28
|
+
<script type="module" src="/rip/browser.js"></script>
|
|
29
|
+
<script type="text/rip">
|
|
30
|
+
{ launch } = importRip! '/rip/ui.rip'
|
|
31
|
+
launch()
|
|
32
|
+
</script>
|
|
284
33
|
```
|
|
285
34
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
Because `component` is an expression (not a declaration), multiple components
|
|
289
|
-
can live in one file:
|
|
35
|
+
**`components/index.rip`** — a component:
|
|
290
36
|
|
|
291
37
|
```coffee
|
|
292
|
-
|
|
293
|
-
@
|
|
294
|
-
@onClick = null
|
|
295
|
-
render
|
|
296
|
-
button.btn @click: @onClick, @label
|
|
297
|
-
|
|
298
|
-
Card = component
|
|
299
|
-
@title = ""
|
|
300
|
-
render
|
|
301
|
-
div.card
|
|
302
|
-
h3 @title
|
|
303
|
-
div.card-body
|
|
304
|
-
slot
|
|
305
|
-
|
|
306
|
-
Page = component
|
|
38
|
+
export Home = component
|
|
39
|
+
@count := 0
|
|
307
40
|
render
|
|
308
41
|
div
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## Virtual File System
|
|
314
|
-
|
|
315
|
-
The VFS is a browser-local file storage layer. Components are delivered as
|
|
316
|
-
source text and stored in memory. The compiler reads from the VFS, not from
|
|
317
|
-
disk.
|
|
318
|
-
|
|
319
|
-
```javascript
|
|
320
|
-
import { vfs } from '@rip-lang/ui/vfs'
|
|
321
|
-
|
|
322
|
-
const fs = vfs()
|
|
323
|
-
|
|
324
|
-
// Write source files
|
|
325
|
-
fs.write('pages/index.rip', 'component Home\n render\n h1 "Hello"')
|
|
326
|
-
fs.write('pages/users/[id].rip', '...')
|
|
327
|
-
|
|
328
|
-
// Read
|
|
329
|
-
fs.read('pages/index.rip') // source string
|
|
330
|
-
fs.exists('pages/index.rip') // true
|
|
331
|
-
fs.list('pages/') // ['index.rip', 'users/']
|
|
332
|
-
fs.listAll('pages/') // all files recursively
|
|
333
|
-
|
|
334
|
-
// Watch for changes (triggers recompilation)
|
|
335
|
-
fs.watch('pages/', ({ event, path }) => {
|
|
336
|
-
console.log(`${event}: ${path}`)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
// Fetch from server
|
|
340
|
-
await fs.fetch('pages/index.rip', '/api/pages/index.rip')
|
|
341
|
-
await fs.fetchManifest([
|
|
342
|
-
'pages/index.rip',
|
|
343
|
-
'pages/about.rip',
|
|
344
|
-
'pages/counter.rip'
|
|
345
|
-
])
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### Why a VFS?
|
|
349
|
-
|
|
350
|
-
Traditional frameworks read from the server's file system during the build step
|
|
351
|
-
and produce static bundles. Rip UI has no build step, so it needs somewhere to
|
|
352
|
-
store source files in the browser. The VFS provides:
|
|
353
|
-
|
|
354
|
-
- **Addressable storage** — components are referenced by path, just like files
|
|
355
|
-
- **File watching** — the renderer re-compiles when a file changes
|
|
356
|
-
- **Lazy loading** — pages can be fetched on demand as the user navigates
|
|
357
|
-
- **Hot update** — write a new version of a file and the component re-renders
|
|
358
|
-
- **Manifest loading** — bulk-fetch an app's files in one call
|
|
359
|
-
|
|
360
|
-
The VFS is not IndexedDB or localStorage — it's a plain in-memory Map. Fast,
|
|
361
|
-
simple, ephemeral. For persistence, the server delivers files on page load.
|
|
362
|
-
|
|
363
|
-
## File-Based Router
|
|
364
|
-
|
|
365
|
-
URLs map to VFS paths. The routing conventions match Next.js / SvelteKit:
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
import { createRouter } from '@rip-lang/ui/router'
|
|
369
|
-
|
|
370
|
-
const router = createRouter(fs, { root: 'pages' })
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
| VFS Path | URL Pattern | Example |
|
|
374
|
-
|----------|-------------|---------|
|
|
375
|
-
| `pages/index.rip` | `/` | Home page |
|
|
376
|
-
| `pages/about.rip` | `/about` | Static page |
|
|
377
|
-
| `pages/users/index.rip` | `/users` | User list |
|
|
378
|
-
| `pages/users/[id].rip` | `/users/:id` | Dynamic segment |
|
|
379
|
-
| `pages/blog/[...slug].rip` | `/blog/*slug` | Catch-all |
|
|
380
|
-
| `pages/_layout.rip` | — | Root layout (wraps all pages) |
|
|
381
|
-
| `pages/users/_layout.rip` | — | Nested layout (wraps `/users/*`) |
|
|
382
|
-
|
|
383
|
-
```javascript
|
|
384
|
-
// Navigate
|
|
385
|
-
router.push('/users/42')
|
|
386
|
-
router.replace('/login')
|
|
387
|
-
router.back()
|
|
388
|
-
|
|
389
|
-
// Reactive route state
|
|
390
|
-
effect(() => {
|
|
391
|
-
console.log(router.path) // '/users/42'
|
|
392
|
-
console.log(router.params) // { id: '42' }
|
|
393
|
-
})
|
|
42
|
+
h1 "Hello from Rip UI"
|
|
43
|
+
button @click: (-> @count += 1), "Clicked #{@count} times"
|
|
394
44
|
```
|
|
395
45
|
|
|
396
|
-
|
|
397
|
-
links and modified clicks (ctrl+click, etc.) pass through normally.
|
|
46
|
+
Run `bun index.rip`, open `http://localhost:3000`.
|
|
398
47
|
|
|
399
|
-
##
|
|
48
|
+
## How It Works
|
|
400
49
|
|
|
401
|
-
|
|
402
|
-
automatically tracked — changing any value triggers fine-grained updates.
|
|
50
|
+
The browser loads two things from the `/rip/` namespace:
|
|
403
51
|
|
|
404
|
-
|
|
405
|
-
|
|
52
|
+
- `/rip/browser.js` — the Rip compiler (~45KB gzip, cached forever)
|
|
53
|
+
- `/rip/ui.rip` — the UI framework (compiled in the browser in ~10-20ms)
|
|
406
54
|
|
|
407
|
-
|
|
408
|
-
user: { name: 'Alice', prefs: { theme: 'dark' } },
|
|
409
|
-
cart: { items: [], total: 0 }
|
|
410
|
-
})
|
|
55
|
+
Then `launch()` fetches the app bundle, hydrates the stash, and renders.
|
|
411
56
|
|
|
412
|
-
|
|
413
|
-
app.user.name // 'Alice'
|
|
414
|
-
app.user.prefs.theme = 'light' // triggers updates
|
|
57
|
+
## The Stash
|
|
415
58
|
|
|
416
|
-
|
|
417
|
-
app.get('user.prefs.theme') // 'light'
|
|
418
|
-
app.set('cart.items[0]', { id: 1 }) // deep set with auto-creation
|
|
419
|
-
app.has('user.name') // true
|
|
420
|
-
app.del('cart.items[0]') // delete
|
|
421
|
-
app.inc('cart.total', 9.99) // increment
|
|
422
|
-
app.merge({ user: { role: 'admin' }}) // shallow merge
|
|
423
|
-
app.keys('user') // ['name', 'prefs', 'role']
|
|
59
|
+
Everything lives in one reactive tree:
|
|
424
60
|
|
|
425
|
-
// Reactive effects
|
|
426
|
-
effect(() => {
|
|
427
|
-
console.log(`Theme: ${app.user.prefs.theme}`)
|
|
428
|
-
// Re-runs whenever theme changes
|
|
429
|
-
})
|
|
430
61
|
```
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
| **App** | Global | Entire session | `app.user`, `app.theme` |
|
|
439
|
-
| **Route** | Per-route | Until navigation | `router.params`, `router.query` |
|
|
440
|
-
| **Component** | Per-instance | Until unmount | `:=` reactive state |
|
|
441
|
-
|
|
442
|
-
App state lives in the stash. Route state lives in the router. Component state
|
|
443
|
-
lives in the component instance as signals. All three are reactive — changes at
|
|
444
|
-
any level trigger the appropriate DOM updates.
|
|
445
|
-
|
|
446
|
-
## Component Renderer
|
|
447
|
-
|
|
448
|
-
The renderer is the bridge between the router and the DOM. When the route
|
|
449
|
-
changes, the renderer:
|
|
450
|
-
|
|
451
|
-
1. Resolves the VFS path for the new route
|
|
452
|
-
2. Reads the `.rip` source from the VFS
|
|
453
|
-
3. Compiles it to JavaScript using the Rip compiler
|
|
454
|
-
4. Evaluates the compiled code to get a component class
|
|
455
|
-
5. Instantiates the component, passing route params as props
|
|
456
|
-
6. Wraps it in any applicable layout components
|
|
457
|
-
7. Mounts the result into the DOM target
|
|
458
|
-
8. Runs transition animations (if configured)
|
|
459
|
-
9. Unmounts the previous component
|
|
460
|
-
|
|
461
|
-
```javascript
|
|
462
|
-
import { createRenderer } from '@rip-lang/ui/renderer'
|
|
463
|
-
|
|
464
|
-
const renderer = createRenderer({
|
|
465
|
-
router,
|
|
466
|
-
fs,
|
|
467
|
-
stash: appState,
|
|
468
|
-
compile: compileToJS,
|
|
469
|
-
target: '#app',
|
|
470
|
-
transition: { duration: 200 }
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
renderer.start() // Watch for route changes, mount components
|
|
474
|
-
renderer.stop() // Unmount everything, clean up
|
|
62
|
+
app
|
|
63
|
+
├── components/ ← component source files
|
|
64
|
+
│ ├── index.rip
|
|
65
|
+
│ ├── counter.rip
|
|
66
|
+
│ └── _layout.rip
|
|
67
|
+
├── routes ← navigation state (path, params, query, hash)
|
|
68
|
+
└── data ← reactive app state (title, theme, user, etc.)
|
|
475
69
|
```
|
|
476
70
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
changes. The VFS watcher triggers cache invalidation, so updating a file in the
|
|
481
|
-
VFS automatically causes the next render to use the new version.
|
|
482
|
-
|
|
483
|
-
## Server Integration
|
|
484
|
-
|
|
485
|
-
### `ripUI` Middleware
|
|
486
|
-
|
|
487
|
-
The `ripUI` export from `@rip-lang/ui/serve` is a setup function that registers
|
|
488
|
-
routes for serving framework files, auto-generated page manifests, and an SSE
|
|
489
|
-
hot-reload channel. It works with `@rip-lang/api`'s `use()`:
|
|
490
|
-
|
|
491
|
-
```coffee
|
|
492
|
-
import { use } from '@rip-lang/api'
|
|
493
|
-
import { ripUI } from '@rip-lang/ui/serve'
|
|
494
|
-
|
|
495
|
-
use ripUI pages: 'pages', watch: true
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
When called, `ripUI` registers the following routes:
|
|
499
|
-
|
|
500
|
-
| Route | Description |
|
|
501
|
-
|-------|-------------|
|
|
502
|
-
| `GET /rip-ui/ui.js` | Framework entry point |
|
|
503
|
-
| `GET /rip-ui/stash.js` | Reactive state module |
|
|
504
|
-
| `GET /rip-ui/vfs.js` | Virtual File System |
|
|
505
|
-
| `GET /rip-ui/router.js` | File-based router |
|
|
506
|
-
| `GET /rip-ui/renderer.js` | Component renderer |
|
|
507
|
-
| `GET /rip-ui/compiler.js` | Rip compiler (40KB) |
|
|
508
|
-
| `GET /rip-ui/manifest.json` | Auto-generated manifest of all `.rip` pages |
|
|
509
|
-
| `GET /rip-ui/watch` | SSE hot-reload endpoint (when `watch: true`) |
|
|
510
|
-
| `GET /pages/*` | Individual `.rip` page files (for hot-reload refetch) |
|
|
71
|
+
Writing to `app.data.theme` updates any component reading it. The stash
|
|
72
|
+
uses Rip's built-in reactive primitives — the same signals that power
|
|
73
|
+
`:=` and `~=` in components.
|
|
511
74
|
|
|
512
|
-
|
|
75
|
+
## The App Bundle
|
|
513
76
|
|
|
514
|
-
|
|
515
|
-
|--------|------|---------|-------------|
|
|
516
|
-
| `pages` | `string` | `'pages'` | Directory containing `.rip` page files |
|
|
517
|
-
| `base` | `string` | `'/rip-ui'` | URL prefix for framework files |
|
|
518
|
-
| `watch` | `boolean` | `false` | Enable SSE hot-reload endpoint |
|
|
519
|
-
| `debounce` | `number` | `250` | Milliseconds to batch filesystem change events |
|
|
520
|
-
|
|
521
|
-
### Page Manifest
|
|
522
|
-
|
|
523
|
-
The manifest endpoint (`/rip-ui/manifest.json`) auto-discovers every `.rip` file
|
|
524
|
-
in the `pages` directory and bundles them into a single JSON response:
|
|
77
|
+
The bundle is JSON served at `/{app}/bundle`. It populates the stash:
|
|
525
78
|
|
|
526
79
|
```json
|
|
527
80
|
{
|
|
528
|
-
"
|
|
529
|
-
|
|
530
|
-
|
|
81
|
+
"components": {
|
|
82
|
+
"components/index.rip": "export Home = component...",
|
|
83
|
+
"components/counter.rip": "export Counter = component..."
|
|
84
|
+
},
|
|
85
|
+
"data": {
|
|
86
|
+
"title": "My App",
|
|
87
|
+
"theme": "light"
|
|
88
|
+
}
|
|
531
89
|
}
|
|
532
90
|
```
|
|
533
91
|
|
|
534
|
-
|
|
535
|
-
populates the VFS in a single request — no need to list pages manually.
|
|
536
|
-
|
|
537
|
-
## Hot Reload
|
|
92
|
+
## Server Middleware
|
|
538
93
|
|
|
539
|
-
The
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
### How It Works
|
|
543
|
-
|
|
544
|
-
```
|
|
545
|
-
Developer saves a .rip file
|
|
546
|
-
│
|
|
547
|
-
▼
|
|
548
|
-
Server (fs.watch)
|
|
549
|
-
├── Debounce (250ms, batches rapid saves)
|
|
550
|
-
└── SSE "changed" event: { paths: ["pages/counter.rip"] }
|
|
551
|
-
│
|
|
552
|
-
▼
|
|
553
|
-
Browser (EventSource)
|
|
554
|
-
├── Invalidate VFS entries (fs.delete)
|
|
555
|
-
├── Rebuild router (router.rebuild)
|
|
556
|
-
├── Smart refetch:
|
|
557
|
-
│ ├── Current page file? → fetch immediately
|
|
558
|
-
│ ├── Active layout? → fetch immediately
|
|
559
|
-
│ ├── Eager file? → fetch immediately
|
|
560
|
-
│ └── Other pages? → fetch lazily on next navigation
|
|
561
|
-
└── Re-render (renderer.remount)
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Server Side
|
|
565
|
-
|
|
566
|
-
The `watch` option enables filesystem monitoring with debounced SSE
|
|
567
|
-
notifications. A heartbeat every 5 seconds keeps the connection alive:
|
|
94
|
+
The `ripUI` middleware registers routes for the framework files, the app
|
|
95
|
+
bundle, and optional SSE hot-reload:
|
|
568
96
|
|
|
569
97
|
```coffee
|
|
570
|
-
use ripUI
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
### Client Side
|
|
574
|
-
|
|
575
|
-
Connect to the SSE endpoint after starting the app:
|
|
576
|
-
|
|
577
|
-
```javascript
|
|
578
|
-
app.watch('/rip-ui/watch');
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
The `watch()` method accepts an optional `eager` array — files that should always
|
|
582
|
-
be refetched immediately, even if they're not the current route:
|
|
583
|
-
|
|
584
|
-
```javascript
|
|
585
|
-
app.watch('/rip-ui/watch', {
|
|
586
|
-
eager: ['pages/_layout.rip']
|
|
587
|
-
});
|
|
98
|
+
use ripUI app: '/demo', dir: dir, title: 'My App'
|
|
588
99
|
```
|
|
589
100
|
|
|
590
|
-
|
|
101
|
+
| Option | Default | Description |
|
|
102
|
+
|--------|---------|-------------|
|
|
103
|
+
| `app` | `''` | URL mount point |
|
|
104
|
+
| `dir` | `'.'` | App directory on disk |
|
|
105
|
+
| `components` | `'components'` | Components subdirectory within `dir` |
|
|
106
|
+
| `watch` | `false` | Enable SSE hot-reload |
|
|
107
|
+
| `debounce` | `250` | Milliseconds to batch file change events |
|
|
108
|
+
| `state` | `null` | Initial app state |
|
|
109
|
+
| `title` | `null` | Document title |
|
|
591
110
|
|
|
592
|
-
|
|
593
|
-
in a single request. Returns the app instance (chainable).
|
|
111
|
+
Routes registered:
|
|
594
112
|
|
|
595
|
-
```javascript
|
|
596
|
-
await app.loadBundle('/rip-ui/manifest.json');
|
|
597
113
|
```
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
called manually:
|
|
604
|
-
|
|
605
|
-
```javascript
|
|
606
|
-
app.renderer.remount();
|
|
114
|
+
/rip/browser.js — Rip compiler
|
|
115
|
+
/rip/ui.rip — UI framework
|
|
116
|
+
/{app}/bundle — app bundle (components + data as JSON)
|
|
117
|
+
/{app}/watch — SSE hot-reload stream (when watch: true)
|
|
118
|
+
/{app}/components/* — individual component files (for hot-reload refetch)
|
|
607
119
|
```
|
|
608
120
|
|
|
609
|
-
##
|
|
121
|
+
## State Preservation (Keep-Alive)
|
|
610
122
|
|
|
611
|
-
|
|
123
|
+
Components are cached when navigating away instead of destroyed. Navigate
|
|
124
|
+
to `/counter`, increment the count, go to `/about`, come back — the count
|
|
125
|
+
is preserved. Configurable via `cacheSize` (default 10).
|
|
612
126
|
|
|
613
|
-
|
|
614
|
-
The middleware serves all framework files, auto-generates a page manifest, and
|
|
615
|
-
provides hot-reload — your server is just a few lines:
|
|
127
|
+
## Data Loading
|
|
616
128
|
|
|
617
|
-
|
|
129
|
+
`createResource` manages async data with reactive `loading`, `error`, and
|
|
130
|
+
`data` properties:
|
|
618
131
|
|
|
619
132
|
```coffee
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
dir = import.meta.dir
|
|
624
|
-
|
|
625
|
-
use ripUI pages: "#{dir}/pages", watch: true
|
|
626
|
-
|
|
627
|
-
get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
|
|
628
|
-
|
|
629
|
-
notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
|
|
630
|
-
|
|
631
|
-
start port: 3000
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
**`index.html`** — The HTML shell:
|
|
635
|
-
|
|
636
|
-
```html
|
|
637
|
-
<!DOCTYPE html>
|
|
638
|
-
<html>
|
|
639
|
-
<head><title>My App</title></head>
|
|
640
|
-
<body>
|
|
641
|
-
<div id="app"></div>
|
|
642
|
-
<script type="module">
|
|
643
|
-
import { compileToJS } from '/rip-ui/compiler.js';
|
|
644
|
-
import { createApp } from '/rip-ui/ui.js';
|
|
645
|
-
|
|
646
|
-
const app = createApp({
|
|
647
|
-
target: '#app',
|
|
648
|
-
compile: compileToJS,
|
|
649
|
-
state: { theme: 'light' }
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
await app.loadBundle('/rip-ui/manifest.json');
|
|
653
|
-
app.start();
|
|
654
|
-
app.watch('/rip-ui/watch');
|
|
655
|
-
</script>
|
|
656
|
-
</body>
|
|
657
|
-
</html>
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
That's it. Run `bun index.rip` and open `http://localhost:3000`. Every `.rip`
|
|
661
|
-
file in the `pages/` directory is auto-discovered, served as a manifest, and
|
|
662
|
-
hot-reloaded on save.
|
|
663
|
-
|
|
664
|
-
### Standalone (No Server)
|
|
133
|
+
export UserPage = component
|
|
134
|
+
user := createResource -> fetch!("/api/users/#{@params.id}").json!
|
|
665
135
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
createApp({
|
|
674
|
-
target: '#app',
|
|
675
|
-
compile: compileToJS,
|
|
676
|
-
files: {
|
|
677
|
-
'pages/index.rip': `
|
|
678
|
-
Home = component
|
|
679
|
-
render
|
|
680
|
-
h1 "Hello, World"
|
|
681
|
-
p "This was compiled in your browser."
|
|
682
|
-
`
|
|
683
|
-
}
|
|
684
|
-
}).start()
|
|
685
|
-
</script>
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
### File Structure
|
|
689
|
-
|
|
690
|
-
```
|
|
691
|
-
my-app/
|
|
692
|
-
├── index.rip # Server (uses @rip-lang/api + ripUI middleware)
|
|
693
|
-
├── index.html # HTML shell
|
|
694
|
-
├── pages/
|
|
695
|
-
│ ├── _layout.rip # Root layout (nav, footer)
|
|
696
|
-
│ ├── index.rip # Home page → /
|
|
697
|
-
│ ├── about.rip # About page → /about
|
|
698
|
-
│ └── users/
|
|
699
|
-
│ ├── _layout.rip # Users layout → wraps /users/*
|
|
700
|
-
│ ├── index.rip # User list → /users
|
|
701
|
-
│ └── [id].rip # User profile → /users/:id
|
|
702
|
-
└── css/
|
|
703
|
-
└── styles.css # Tailwind or plain CSS
|
|
136
|
+
render
|
|
137
|
+
if user.loading
|
|
138
|
+
p "Loading..."
|
|
139
|
+
else if user.error
|
|
140
|
+
p "Error: #{user.error.message}"
|
|
141
|
+
else
|
|
142
|
+
h1 user.data.name
|
|
704
143
|
```
|
|
705
144
|
|
|
706
|
-
|
|
707
|
-
> automatically by the `ripUI` middleware from the installed `@rip-lang/ui`
|
|
708
|
-
> package — you don't need to copy them into your project.
|
|
709
|
-
|
|
710
|
-
## Render Template Syntax
|
|
711
|
-
|
|
712
|
-
The `render` block uses a concise, indentation-based template syntax:
|
|
145
|
+
## Error Boundaries
|
|
713
146
|
|
|
714
|
-
|
|
147
|
+
Layouts with an `onError` method catch errors from child components:
|
|
715
148
|
|
|
716
149
|
```coffee
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
div.card # <div class="card"></div>
|
|
720
|
-
div.card.active # <div class="card active"></div>
|
|
721
|
-
button.btn.btn-primary # <button class="btn btn-primary"></button>
|
|
722
|
-
```
|
|
150
|
+
export Layout = component
|
|
151
|
+
errorMsg := null
|
|
723
152
|
|
|
724
|
-
|
|
153
|
+
onError: (err) -> errorMsg = err.message
|
|
725
154
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
155
|
+
render
|
|
156
|
+
.app-layout
|
|
157
|
+
if errorMsg
|
|
158
|
+
.error-banner "#{errorMsg}"
|
|
159
|
+
#content
|
|
731
160
|
```
|
|
732
161
|
|
|
733
|
-
|
|
734
|
-
out. This provides native CLSX-like behavior without a library.
|
|
162
|
+
## Navigation Indicator
|
|
735
163
|
|
|
736
|
-
|
|
164
|
+
`router.navigating` is a reactive signal — true while a route transition
|
|
165
|
+
is in progress:
|
|
737
166
|
|
|
738
167
|
```coffee
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
button @click: @handleClick, "Submit"
|
|
742
|
-
a href: "/about", "About Us"
|
|
743
|
-
img src: @imageUrl, alt: "Photo"
|
|
168
|
+
if @router.navigating
|
|
169
|
+
span "Loading..."
|
|
744
170
|
```
|
|
745
171
|
|
|
746
|
-
|
|
172
|
+
## Multi-App Hosting
|
|
747
173
|
|
|
748
|
-
|
|
749
|
-
render
|
|
750
|
-
h1 "Static text"
|
|
751
|
-
p "Hello, #{@name}"
|
|
752
|
-
span "Count: #{@count}"
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
### Conditionals
|
|
174
|
+
Mount multiple apps under one server:
|
|
756
175
|
|
|
757
176
|
```coffee
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
else
|
|
762
|
-
p "Please log in"
|
|
763
|
-
```
|
|
177
|
+
import { get, start, notFound } from '@rip-lang/api'
|
|
178
|
+
import { mount as demo } from './demo/index.rip'
|
|
179
|
+
import { mount as labs } from './labs/index.rip'
|
|
764
180
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
ul
|
|
770
|
-
for item in @items
|
|
771
|
-
li item.name
|
|
181
|
+
demo '/demo'
|
|
182
|
+
labs '/labs'
|
|
183
|
+
get '/', -> Response.redirect('/demo/', 302)
|
|
184
|
+
start port: 3002
|
|
772
185
|
```
|
|
773
186
|
|
|
774
|
-
|
|
187
|
+
Each app is a directory with `components/`, `css/`, `index.html`, and `index.rip`.
|
|
188
|
+
The `/rip/` namespace is shared — all apps use the same compiler and framework.
|
|
775
189
|
|
|
776
|
-
|
|
190
|
+
## File Structure
|
|
777
191
|
|
|
778
|
-
```coffee
|
|
779
|
-
render
|
|
780
|
-
div.app
|
|
781
|
-
header.app-header
|
|
782
|
-
h1 "My App"
|
|
783
|
-
nav
|
|
784
|
-
a href: "/", "Home"
|
|
785
|
-
a href: "/about", "About"
|
|
786
|
-
main.app-body
|
|
787
|
-
p "Content goes here"
|
|
788
|
-
footer
|
|
789
|
-
p "Footer"
|
|
790
192
|
```
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
| `transition` | `object` | — | Route transition `{ duration }` |
|
|
804
|
-
| `onError` | `function` | — | Error handler |
|
|
805
|
-
| `onNavigate` | `function` | — | Navigation callback |
|
|
806
|
-
|
|
807
|
-
Returns an instance with these methods:
|
|
808
|
-
|
|
809
|
-
| Method | Description |
|
|
810
|
-
|--------|-------------|
|
|
811
|
-
| `start()` | Start routing and rendering |
|
|
812
|
-
| `stop()` | Unmount components, close SSE connection, clean up |
|
|
813
|
-
| `load(paths)` | Fetch `.rip` files from server into VFS |
|
|
814
|
-
| `loadBundle(url)` | Fetch a JSON manifest and bulk-load all pages into VFS |
|
|
815
|
-
| `watch(url, opts?)` | Connect to SSE hot-reload endpoint |
|
|
816
|
-
| `go(path)` | Navigate to a route |
|
|
817
|
-
| `addPage(path, source)` | Add a page to the VFS |
|
|
818
|
-
| `get(key)` | Get app state value |
|
|
819
|
-
| `set(key, value)` | Set app state value |
|
|
820
|
-
|
|
821
|
-
Also exposes: `app` (stash), `fs` (VFS), `router`, `renderer`
|
|
822
|
-
|
|
823
|
-
### `stash(data)`
|
|
824
|
-
|
|
825
|
-
Creates a deeply reactive proxy around `data`. Every property read is tracked,
|
|
826
|
-
every write triggers effects.
|
|
827
|
-
|
|
828
|
-
### `effect(fn)`
|
|
829
|
-
|
|
830
|
-
Creates a side effect that re-runs whenever its tracked dependencies change.
|
|
831
|
-
|
|
832
|
-
### `computed(fn)`
|
|
833
|
-
|
|
834
|
-
Creates a lazy computed value that caches until dependencies change.
|
|
835
|
-
|
|
836
|
-
### `batch(fn)`
|
|
837
|
-
|
|
838
|
-
Groups multiple state updates — effects only fire once at the end.
|
|
839
|
-
|
|
840
|
-
## Design Principles
|
|
841
|
-
|
|
842
|
-
**No build step.** The compiler is small enough to ship. Source files are the
|
|
843
|
-
deployment artifact.
|
|
844
|
-
|
|
845
|
-
**Language-native reactivity.** `:=` for state, `~=` for computed, `~>` for
|
|
846
|
-
effects. These are Rip language features, not framework APIs.
|
|
847
|
-
|
|
848
|
-
**Fine-grained DOM updates.** No virtual DOM. Each reactive binding creates a
|
|
849
|
-
direct effect that updates exactly the DOM nodes it touches.
|
|
850
|
-
|
|
851
|
-
**Components are classes.** `component` produces an anonymous ES6 class.
|
|
852
|
-
Methods, lifecycle hooks, and state are ordinary class members. No hooks API, no
|
|
853
|
-
composition functions, no magic — just a class with a `render` method.
|
|
854
|
-
|
|
855
|
-
**Props are instance variables.** `@count := 0` defines a reactive prop. The
|
|
856
|
-
parent can set it, ignore it, or share a signal. The child owns it.
|
|
857
|
-
|
|
858
|
-
**File-based everything.** Components live in the VFS. Routes map to VFS paths.
|
|
859
|
-
Layouts are `_layout.rip` files in the directory tree. The file system is the
|
|
860
|
-
API.
|
|
193
|
+
my-app/
|
|
194
|
+
├── index.rip # Server
|
|
195
|
+
├── index.html # HTML page
|
|
196
|
+
├── components/
|
|
197
|
+
│ ├── _layout.rip # Root layout
|
|
198
|
+
│ ├── index.rip # Home → /
|
|
199
|
+
│ ├── about.rip # About → /about
|
|
200
|
+
│ └── users/
|
|
201
|
+
│ └── [id].rip # User profile → /users/:id
|
|
202
|
+
└── css/
|
|
203
|
+
└── styles.css # Styles
|
|
204
|
+
```
|
|
861
205
|
|
|
862
206
|
## License
|
|
863
207
|
|
|
864
208
|
MIT
|
|
865
|
-
|
|
866
|
-
## Requirements
|
|
867
|
-
|
|
868
|
-
- [Bun](https://bun.sh) runtime
|
|
869
|
-
- `@rip-lang/api` ≥ 1.1.4 (for `@send` file serving used by `ripUI` middleware)
|
|
870
|
-
- `rip-lang` ≥ 3.1.1 (Rip compiler with browser build)
|
|
871
|
-
|
|
872
|
-
## Links
|
|
873
|
-
|
|
874
|
-
- [Rip Language](https://github.com/shreeve/rip-lang)
|
|
875
|
-
- [@rip-lang/api](../api/README.md) — API framework (routing, middleware, `@send`)
|
|
876
|
-
- [@rip-lang/server](../server/README.md) — Production server (HTTPS, mDNS, multi-worker)
|
|
877
|
-
- [Report Issues](https://github.com/shreeve/rip-lang/issues)
|