@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 CHANGED
@@ -1,877 +1,208 @@
1
- <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
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
- > **A zero-build reactive web framework ship the compiler to the browser, compile on demand, render with fine-grained reactivity**
5
+ > **Zero-build reactive web framework for the Rip language.**
6
6
 
7
- Rip UI inverts the traditional web development model. Instead of building,
8
- bundling, and shipping static JavaScript artifacts to the browser, it ships the
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
- The component model adds exactly **two keywords** to the Rip language —
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
- Components can nest. Props are passed as object arguments:
12
+ **`index.rip`** the server:
262
13
 
263
14
  ```coffee
264
- App = component
265
- @user := { name: "Alice" }
15
+ import { get, use, start, notFound } from '@rip-lang/api'
16
+ import { ripUI } from '@rip-lang/ui/serve'
266
17
 
267
- render
268
- div
269
- Header {title: "My App"}
270
- UserCard {name: @user.name, avatar: "/alice.png"}
271
- Footer
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
- ### Context
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
- # In any descendant component:
283
- theme = getContext 'theme'
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
- ### Multiple Components Per File
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
- Button = component
293
- @label = "Click"
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
- Card {title: "Welcome"}
310
- Button {label: "Get Started", onClick: -> alert "Go!"}
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
- The router intercepts `<a>` clicks automatically for SPA navigation. External
397
- links and modified clicks (ctrl+click, etc.) pass through normally.
46
+ Run `bun index.rip`, open `http://localhost:3000`.
398
47
 
399
- ## Reactive Stash
48
+ ## How It Works
400
49
 
401
- Deep reactive state tree with path-based navigation. Every nested property is
402
- automatically tracked — changing any value triggers fine-grained updates.
50
+ The browser loads two things from the `/rip/` namespace:
403
51
 
404
- ```javascript
405
- import { stash, effect } from '@rip-lang/ui/stash'
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
- const app = stash({
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
- // Direct property access (tracked)
413
- app.user.name // 'Alice'
414
- app.user.prefs.theme = 'light' // triggers updates
57
+ ## The Stash
415
58
 
416
- // Path-based access
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
- ### State Tiers
433
-
434
- Three levels of reactive state, each scoped appropriately:
435
-
436
- | Tier | Scope | Lifetime | Access |
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
- ### Compilation Cache
478
-
479
- Compiled components are cached by VFS path. A file is only recompiled when it
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
- ### Options
75
+ ## The App Bundle
513
76
 
514
- | Option | Type | Default | Description |
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
- "pages/index.rip": "Home = component\n render\n h1 \"Hello\"",
529
- "pages/about.rip": "About = component\n render\n h1 \"About\"",
530
- "pages/counter.rip": "..."
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
- The client loads this with `app.loadBundle('/rip-ui/manifest.json')`, which
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 hot-reload system uses a **notify-only** architecture — the server tells the
540
- browser *which files changed*, then the browser decides what to refetch.
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 pages: "#{dir}/pages", watch: true, debounce: 250
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
- ### `loadBundle(url)`
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
- Fetches a JSON manifest containing all page sources and loads them into the VFS
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
- ### `remount()`
600
-
601
- The renderer's `remount()` method re-renders the current route. This is called
602
- automatically by `watch()` after refetching changed files, but it can also be
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
- ## Quick Start
121
+ ## State Preservation (Keep-Alive)
610
122
 
611
- ### With Server Integration (Recommended)
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
- The fastest way to get started is with `@rip-lang/api` and the `ripUI` middleware.
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
- **`index.rip`** The complete server:
129
+ `createResource` manages async data with reactive `loading`, `error`, and
130
+ `data` properties:
618
131
 
619
132
  ```coffee
620
- import { get, use, start, notFound } from '@rip-lang/api'
621
- import { ripUI } from '@rip-lang/ui/serve'
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
- For static deployments or quick prototyping, you can inline components directly:
667
-
668
- ```html
669
- <script type="module">
670
- import { compileToJS } from './rip.browser.js'
671
- import { createApp } from './ui.js'
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
- > **Note:** Framework files (`ui.js`, `stash.js`, `router.js`, etc.) are served
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
- ### Tags and Classes
147
+ Layouts with an `onError` method catch errors from child components:
715
148
 
716
149
  ```coffee
717
- render
718
- div # <div></div>
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
- ### Dynamic Classes (CLSX)
153
+ onError: (err) -> errorMsg = err.message
725
154
 
726
- ```coffee
727
- render
728
- div.("active" if @selected) # conditional class
729
- div.("bg-red" if error, "bg-green" if ok) # multiple conditions
730
- div.card.("highlighted" if @featured) # static + dynamic
155
+ render
156
+ .app-layout
157
+ if errorMsg
158
+ .error-banner "#{errorMsg}"
159
+ #content
731
160
  ```
732
161
 
733
- Dynamic class expressions are evaluated at runtime. Falsy values are filtered
734
- out. This provides native CLSX-like behavior without a library.
162
+ ## Navigation Indicator
735
163
 
736
- ### Attributes and Events
164
+ `router.navigating` is a reactive signal — true while a route transition
165
+ is in progress:
737
166
 
738
167
  ```coffee
739
- render
740
- input type: "text", placeholder: "Search..."
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
- ### Text and Interpolation
172
+ ## Multi-App Hosting
747
173
 
748
- ```coffee
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
- render
759
- if @loggedIn
760
- p "Welcome back, #{@name}"
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
- ### Loops
766
-
767
- ```coffee
768
- render
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
- ### Nesting
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
- Indentation defines parent-child relationships:
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
- ## API Reference
793
-
794
- ### `createApp(options)`
795
-
796
- | Option | Type | Default | Description |
797
- |--------|------|---------|-------------|
798
- | `target` | `string\|Element` | `'#app'` | DOM mount target |
799
- | `state` | `object` | `{}` | Initial app state (becomes reactive stash) |
800
- | `files` | `object` | `{}` | Initial VFS files `{ path: content }` |
801
- | `root` | `string` | `'pages'` | Pages directory in VFS |
802
- | `compile` | `function` | — | Rip compiler (`compileToJS`) |
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)