@rip-lang/ui 0.1.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/README.md +694 -0
- package/package.json +61 -0
- package/renderer.js +397 -0
- package/router.js +325 -0
- package/serve.rip +143 -0
- package/stash.js +413 -0
- package/ui.js +208 -0
- package/vfs.js +215 -0
package/README.md
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
|
|
2
|
+
|
|
3
|
+
# Rip UI - @rip-lang/ui
|
|
4
|
+
|
|
5
|
+
> **A zero-build reactive web framework — ship the compiler to the browser, compile on demand, render with fine-grained reactivity**
|
|
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.
|
|
13
|
+
|
|
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
|
+
Browser loads: rip.browser.js (40KB) + @rip-lang/ui (~8KB)
|
|
22
|
+
│
|
|
23
|
+
┌────────────────┼────────────────┐
|
|
24
|
+
│ │ │
|
|
25
|
+
Reactive Stash Virtual FS Router
|
|
26
|
+
(app state) (file storage) (URL → VFS)
|
|
27
|
+
│ │ │
|
|
28
|
+
└────────────────┼────────────────┘
|
|
29
|
+
│
|
|
30
|
+
Renderer
|
|
31
|
+
(compiles + mounts)
|
|
32
|
+
│
|
|
33
|
+
DOM
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Module | Size | Role |
|
|
37
|
+
|--------|------|------|
|
|
38
|
+
| `ui.js` | ~150 lines | `createApp` entry point, re-exports everything |
|
|
39
|
+
| `stash.js` | ~400 lines | Deep reactive state tree with path-based navigation |
|
|
40
|
+
| `vfs.js` | ~200 lines | Browser-local Virtual File System with watchers |
|
|
41
|
+
| `router.js` | ~300 lines | File-based router (URL ↔ VFS paths, History API) |
|
|
42
|
+
| `renderer.js` | ~250 lines | Component lifecycle, layouts, transitions |
|
|
43
|
+
|
|
44
|
+
## The Idea
|
|
45
|
+
|
|
46
|
+
Modern web frameworks — React, Vue, Svelte, Solid — all share the same
|
|
47
|
+
fundamental assumption: code must be compiled and bundled on the developer's
|
|
48
|
+
machine, then shipped as static artifacts. This creates an entire ecosystem of
|
|
49
|
+
build tools (Vite, Webpack, Turbopack, esbuild), configuration files, dev
|
|
50
|
+
servers, hot module replacement protocols, and deployment pipelines. The
|
|
51
|
+
developer experience is powerful, but the machinery is enormous.
|
|
52
|
+
|
|
53
|
+
Rip UI asks: **what if the compiler ran in the browser?**
|
|
54
|
+
|
|
55
|
+
At 40KB, the Rip compiler is small enough to ship alongside your application.
|
|
56
|
+
Components arrive as `.rip` source files — plain text — and are compiled to
|
|
57
|
+
JavaScript on the client's machine. This eliminates the build step entirely.
|
|
58
|
+
There is no `dist/` folder, no source maps, no chunk splitting, no tree
|
|
59
|
+
shaking, no CI build minutes. You write a `.rip` file, the browser compiles it,
|
|
60
|
+
and it runs.
|
|
61
|
+
|
|
62
|
+
This is not a toy or a limitation. The compiler produces the same output it
|
|
63
|
+
would on a server. The reactive system is the same signal-based engine that
|
|
64
|
+
powers server-side Rip. The component model compiles to anonymous ES6 classes
|
|
65
|
+
with fine-grained DOM manipulation — no virtual DOM diffing.
|
|
66
|
+
|
|
67
|
+
### How It Differs from Existing Frameworks
|
|
68
|
+
|
|
69
|
+
| | React/Vue/Svelte | Rip UI |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| **Build step** | Required (Vite, Webpack, etc.) | None — compiler runs in browser |
|
|
72
|
+
| **Bundle size** | 40-100KB+ framework + app bundle | 40KB compiler + ~8KB framework + raw source |
|
|
73
|
+
| **HMR** | Dev server ↔ browser WebSocket | Not needed — recompile in-place |
|
|
74
|
+
| **Deployment** | Build artifacts (`dist/`) | Source files served as-is |
|
|
75
|
+
| **Component format** | JSX, SFC, templates | Rip source (`.rip` files) |
|
|
76
|
+
| **Reactivity** | Library-specific (hooks, refs, signals) | Language-native (`:=`, `~=`, `~>`) |
|
|
77
|
+
| **DOM updates** | Virtual DOM diff or compiled transforms | Fine-grained effects, direct DOM mutation |
|
|
78
|
+
| **Routing** | Framework plugin (react-router, etc.) | Built-in file-based router over VFS |
|
|
79
|
+
| **State management** | External library (Redux, Pinia, etc.) | Built-in reactive stash with deep tracking |
|
|
80
|
+
|
|
81
|
+
## Component Model
|
|
82
|
+
|
|
83
|
+
The component system adds two keywords to Rip: `component` and `render`. Everything
|
|
84
|
+
else — reactive state (`:=`), computed values (`~=`), effects (`~>`), methods,
|
|
85
|
+
lifecycle — uses standard Rip syntax. A component is an expression that evaluates
|
|
86
|
+
to an anonymous ES6 class.
|
|
87
|
+
|
|
88
|
+
### Defining a Component
|
|
89
|
+
|
|
90
|
+
```coffee
|
|
91
|
+
Counter = component
|
|
92
|
+
@count := 0 # reactive state (signal) — parent can override via props
|
|
93
|
+
@step = 1 # plain prop — parent can set, not reactive
|
|
94
|
+
|
|
95
|
+
doubled ~= @count * 2 # computed (derived, read-only)
|
|
96
|
+
|
|
97
|
+
increment: -> @count += @step
|
|
98
|
+
decrement: -> @count -= @step
|
|
99
|
+
|
|
100
|
+
mounted: ->
|
|
101
|
+
console.log "Counter mounted"
|
|
102
|
+
|
|
103
|
+
render
|
|
104
|
+
div.counter
|
|
105
|
+
h1 "Count: #{@count}"
|
|
106
|
+
p "Doubled: #{doubled}"
|
|
107
|
+
button @click: @increment, "+#{@step}"
|
|
108
|
+
button @click: @decrement, "-#{@step}"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This compiles to an anonymous ES6 class expression:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
Counter = class {
|
|
115
|
+
constructor(props = {}) {
|
|
116
|
+
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
117
|
+
this.step = props.step ?? 1;
|
|
118
|
+
this.doubled = __computed(() => this.count.value * 2);
|
|
119
|
+
}
|
|
120
|
+
increment() { return this.count.value += this.step; }
|
|
121
|
+
decrement() { return this.count.value -= this.step; }
|
|
122
|
+
mounted() { return console.log("Counter mounted"); }
|
|
123
|
+
_create() { /* fine-grained DOM creation */ }
|
|
124
|
+
_setup() { /* reactive effect bindings */ }
|
|
125
|
+
mount(target) { /* ... */ }
|
|
126
|
+
unmount() { /* ... */ }
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### The Two Keywords
|
|
131
|
+
|
|
132
|
+
**`component`** — Declares an anonymous class with component semantics:
|
|
133
|
+
|
|
134
|
+
- `@` properties become instance variables that the parent can set via props
|
|
135
|
+
- `:=` assignments create reactive signals (`__state`)
|
|
136
|
+
- `~=` assignments create computed values (`__computed`)
|
|
137
|
+
- `~>` assignments create effects (`__effect`)
|
|
138
|
+
- Plain `=` assignments with function values become methods
|
|
139
|
+
- `mounted`, `unmounted`, `updated` are lifecycle hooks called by the runtime
|
|
140
|
+
- Everything else is standard Rip class behavior
|
|
141
|
+
|
|
142
|
+
**`render`** — Defines the component's template using a Pug/Jade-like DSL:
|
|
143
|
+
|
|
144
|
+
- Tags are bare identifiers: `div`, `h1`, `button`, `span`
|
|
145
|
+
- Classes use dot notation: `div.card.active`, `button.btn.btn-primary`
|
|
146
|
+
- Dynamic classes use dot-parens: `div.("active" if @selected)` (CLSX-like)
|
|
147
|
+
- Attributes use object syntax: `input type: "text", placeholder: "..."`
|
|
148
|
+
- Events use `@` prefix: `button @click: @handleClick`
|
|
149
|
+
- Text content is a string argument: `h1 "Hello"`
|
|
150
|
+
- Interpolation works: `p "Count: #{@count}"`
|
|
151
|
+
- Children are indented below their parent
|
|
152
|
+
- `if`/`else` and `for...in` work inside templates
|
|
153
|
+
|
|
154
|
+
That's it. No special attribute syntax, no directive system, no template
|
|
155
|
+
compiler — just Rip's existing syntax applied to DOM construction.
|
|
156
|
+
|
|
157
|
+
### Props
|
|
158
|
+
|
|
159
|
+
Every `@` property on a component is a prop that the parent can set. The child
|
|
160
|
+
owns the property; the parent can provide an initial value or pass a reactive
|
|
161
|
+
signal:
|
|
162
|
+
|
|
163
|
+
```coffee
|
|
164
|
+
# Parent passes plain value — child wraps it in a signal
|
|
165
|
+
Counter {count: 10}
|
|
166
|
+
|
|
167
|
+
# Parent passes its own signal — child uses it directly (shared state)
|
|
168
|
+
Counter {count: parentCount}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The `isSignal` check in the constructor handles this automatically:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If you don't want the parent to override a prop, don't use `@`:
|
|
178
|
+
|
|
179
|
+
```coffee
|
|
180
|
+
MyComponent = component
|
|
181
|
+
active := false # internal state — not a prop, parent can't set it
|
|
182
|
+
@count := 0 # prop — parent can set or share a signal
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Required and Optional Props
|
|
186
|
+
|
|
187
|
+
```coffee
|
|
188
|
+
UserCard = component
|
|
189
|
+
@name # required — no default, error if missing
|
|
190
|
+
@avatar = "/default.png" # optional — has a default
|
|
191
|
+
@bio? := "" # optional reactive — ? makes it explicitly optional
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Computed Values and Effects
|
|
195
|
+
|
|
196
|
+
```coffee
|
|
197
|
+
TodoList = component
|
|
198
|
+
@todos := []
|
|
199
|
+
remaining ~= @todos.filter((t) -> not t.done).length
|
|
200
|
+
total ~= @todos.length
|
|
201
|
+
|
|
202
|
+
~> console.log "#{remaining} of #{total} remaining"
|
|
203
|
+
|
|
204
|
+
render
|
|
205
|
+
div
|
|
206
|
+
h2 "Todos (#{remaining}/#{total})"
|
|
207
|
+
# ...
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Lifecycle
|
|
211
|
+
|
|
212
|
+
`mounted`, `unmounted`, and `updated` are just methods. No special syntax. The
|
|
213
|
+
runtime calls them at the appropriate times:
|
|
214
|
+
|
|
215
|
+
```coffee
|
|
216
|
+
Timer = component
|
|
217
|
+
@elapsed := 0
|
|
218
|
+
@interval = null
|
|
219
|
+
|
|
220
|
+
mounted: ->
|
|
221
|
+
@interval = setInterval (=> @elapsed += 1), 1000
|
|
222
|
+
|
|
223
|
+
unmounted: ->
|
|
224
|
+
clearInterval @interval
|
|
225
|
+
|
|
226
|
+
render
|
|
227
|
+
p "#{@elapsed} seconds"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Two-Way Binding
|
|
231
|
+
|
|
232
|
+
The `<=>` operator creates two-way bindings between form elements and reactive
|
|
233
|
+
state:
|
|
234
|
+
|
|
235
|
+
```coffee
|
|
236
|
+
SearchBox = component
|
|
237
|
+
@query := ""
|
|
238
|
+
|
|
239
|
+
render
|
|
240
|
+
input type: "text", value <=> @query
|
|
241
|
+
p "Searching for: #{@query}"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Child Components
|
|
245
|
+
|
|
246
|
+
Components can nest. Props are passed as object arguments:
|
|
247
|
+
|
|
248
|
+
```coffee
|
|
249
|
+
App = component
|
|
250
|
+
@user := { name: "Alice" }
|
|
251
|
+
|
|
252
|
+
render
|
|
253
|
+
div
|
|
254
|
+
Header {title: "My App"}
|
|
255
|
+
UserCard {name: @user.name, avatar: "/alice.png"}
|
|
256
|
+
Footer
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Context
|
|
260
|
+
|
|
261
|
+
Components can share state down the tree without passing props at every level:
|
|
262
|
+
|
|
263
|
+
```coffee
|
|
264
|
+
# In a parent component's constructor:
|
|
265
|
+
setContext 'theme', @theme
|
|
266
|
+
|
|
267
|
+
# In any descendant component:
|
|
268
|
+
theme = getContext 'theme'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Multiple Components Per File
|
|
272
|
+
|
|
273
|
+
Because `component` is an expression (not a declaration), multiple components
|
|
274
|
+
can live in one file:
|
|
275
|
+
|
|
276
|
+
```coffee
|
|
277
|
+
Button = component
|
|
278
|
+
@label = "Click"
|
|
279
|
+
@onClick = null
|
|
280
|
+
render
|
|
281
|
+
button.btn @click: @onClick, @label
|
|
282
|
+
|
|
283
|
+
Card = component
|
|
284
|
+
@title = ""
|
|
285
|
+
render
|
|
286
|
+
div.card
|
|
287
|
+
h3 @title
|
|
288
|
+
div.card-body
|
|
289
|
+
slot
|
|
290
|
+
|
|
291
|
+
Page = component
|
|
292
|
+
render
|
|
293
|
+
div
|
|
294
|
+
Card {title: "Welcome"}
|
|
295
|
+
Button {label: "Get Started", onClick: -> alert "Go!"}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Virtual File System
|
|
299
|
+
|
|
300
|
+
The VFS is a browser-local file storage layer. Components are delivered as
|
|
301
|
+
source text and stored in memory. The compiler reads from the VFS, not from
|
|
302
|
+
disk.
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
import { vfs } from '@rip-lang/ui/vfs'
|
|
306
|
+
|
|
307
|
+
const fs = vfs()
|
|
308
|
+
|
|
309
|
+
// Write source files
|
|
310
|
+
fs.write('pages/index.rip', 'component Home\n render\n h1 "Hello"')
|
|
311
|
+
fs.write('pages/users/[id].rip', '...')
|
|
312
|
+
|
|
313
|
+
// Read
|
|
314
|
+
fs.read('pages/index.rip') // source string
|
|
315
|
+
fs.exists('pages/index.rip') // true
|
|
316
|
+
fs.list('pages/') // ['index.rip', 'users/']
|
|
317
|
+
fs.listAll('pages/') // all files recursively
|
|
318
|
+
|
|
319
|
+
// Watch for changes (triggers recompilation)
|
|
320
|
+
fs.watch('pages/', ({ event, path }) => {
|
|
321
|
+
console.log(`${event}: ${path}`)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Fetch from server
|
|
325
|
+
await fs.fetch('pages/index.rip', '/api/pages/index.rip')
|
|
326
|
+
await fs.fetchManifest([
|
|
327
|
+
'pages/index.rip',
|
|
328
|
+
'pages/about.rip',
|
|
329
|
+
'pages/counter.rip'
|
|
330
|
+
])
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Why a VFS?
|
|
334
|
+
|
|
335
|
+
Traditional frameworks read from the server's file system during the build step
|
|
336
|
+
and produce static bundles. Rip UI has no build step, so it needs somewhere to
|
|
337
|
+
store source files in the browser. The VFS provides:
|
|
338
|
+
|
|
339
|
+
- **Addressable storage** — components are referenced by path, just like files
|
|
340
|
+
- **File watching** — the renderer re-compiles when a file changes
|
|
341
|
+
- **Lazy loading** — pages can be fetched on demand as the user navigates
|
|
342
|
+
- **Hot update** — write a new version of a file and the component re-renders
|
|
343
|
+
- **Manifest loading** — bulk-fetch an app's files in one call
|
|
344
|
+
|
|
345
|
+
The VFS is not IndexedDB or localStorage — it's a plain in-memory Map. Fast,
|
|
346
|
+
simple, ephemeral. For persistence, the server delivers files on page load.
|
|
347
|
+
|
|
348
|
+
## File-Based Router
|
|
349
|
+
|
|
350
|
+
URLs map to VFS paths. The routing conventions match Next.js / SvelteKit:
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
import { createRouter } from '@rip-lang/ui/router'
|
|
354
|
+
|
|
355
|
+
const router = createRouter(fs, { root: 'pages' })
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| VFS Path | URL Pattern | Example |
|
|
359
|
+
|----------|-------------|---------|
|
|
360
|
+
| `pages/index.rip` | `/` | Home page |
|
|
361
|
+
| `pages/about.rip` | `/about` | Static page |
|
|
362
|
+
| `pages/users/index.rip` | `/users` | User list |
|
|
363
|
+
| `pages/users/[id].rip` | `/users/:id` | Dynamic segment |
|
|
364
|
+
| `pages/blog/[...slug].rip` | `/blog/*slug` | Catch-all |
|
|
365
|
+
| `pages/_layout.rip` | — | Root layout (wraps all pages) |
|
|
366
|
+
| `pages/users/_layout.rip` | — | Nested layout (wraps `/users/*`) |
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
// Navigate
|
|
370
|
+
router.push('/users/42')
|
|
371
|
+
router.replace('/login')
|
|
372
|
+
router.back()
|
|
373
|
+
|
|
374
|
+
// Reactive route state
|
|
375
|
+
effect(() => {
|
|
376
|
+
console.log(router.path) // '/users/42'
|
|
377
|
+
console.log(router.params) // { id: '42' }
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The router intercepts `<a>` clicks automatically for SPA navigation. External
|
|
382
|
+
links and modified clicks (ctrl+click, etc.) pass through normally.
|
|
383
|
+
|
|
384
|
+
## Reactive Stash
|
|
385
|
+
|
|
386
|
+
Deep reactive state tree with path-based navigation. Every nested property is
|
|
387
|
+
automatically tracked — changing any value triggers fine-grained updates.
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
import { stash, effect } from '@rip-lang/ui/stash'
|
|
391
|
+
|
|
392
|
+
const app = stash({
|
|
393
|
+
user: { name: 'Alice', prefs: { theme: 'dark' } },
|
|
394
|
+
cart: { items: [], total: 0 }
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// Direct property access (tracked)
|
|
398
|
+
app.user.name // 'Alice'
|
|
399
|
+
app.user.prefs.theme = 'light' // triggers updates
|
|
400
|
+
|
|
401
|
+
// Path-based access
|
|
402
|
+
app.get('user.prefs.theme') // 'light'
|
|
403
|
+
app.set('cart.items[0]', { id: 1 }) // deep set with auto-creation
|
|
404
|
+
app.has('user.name') // true
|
|
405
|
+
app.del('cart.items[0]') // delete
|
|
406
|
+
app.inc('cart.total', 9.99) // increment
|
|
407
|
+
app.merge({ user: { role: 'admin' }}) // shallow merge
|
|
408
|
+
app.keys('user') // ['name', 'prefs', 'role']
|
|
409
|
+
|
|
410
|
+
// Reactive effects
|
|
411
|
+
effect(() => {
|
|
412
|
+
console.log(`Theme: ${app.user.prefs.theme}`)
|
|
413
|
+
// Re-runs whenever theme changes
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### State Tiers
|
|
418
|
+
|
|
419
|
+
Three levels of reactive state, each scoped appropriately:
|
|
420
|
+
|
|
421
|
+
| Tier | Scope | Lifetime | Access |
|
|
422
|
+
|------|-------|----------|--------|
|
|
423
|
+
| **App** | Global | Entire session | `app.user`, `app.theme` |
|
|
424
|
+
| **Route** | Per-route | Until navigation | `router.params`, `router.query` |
|
|
425
|
+
| **Component** | Per-instance | Until unmount | `:=` reactive state |
|
|
426
|
+
|
|
427
|
+
App state lives in the stash. Route state lives in the router. Component state
|
|
428
|
+
lives in the component instance as signals. All three are reactive — changes at
|
|
429
|
+
any level trigger the appropriate DOM updates.
|
|
430
|
+
|
|
431
|
+
## Component Renderer
|
|
432
|
+
|
|
433
|
+
The renderer is the bridge between the router and the DOM. When the route
|
|
434
|
+
changes, the renderer:
|
|
435
|
+
|
|
436
|
+
1. Resolves the VFS path for the new route
|
|
437
|
+
2. Reads the `.rip` source from the VFS
|
|
438
|
+
3. Compiles it to JavaScript using the Rip compiler
|
|
439
|
+
4. Evaluates the compiled code to get a component class
|
|
440
|
+
5. Instantiates the component, passing route params as props
|
|
441
|
+
6. Wraps it in any applicable layout components
|
|
442
|
+
7. Mounts the result into the DOM target
|
|
443
|
+
8. Runs transition animations (if configured)
|
|
444
|
+
9. Unmounts the previous component
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
import { createRenderer } from '@rip-lang/ui/renderer'
|
|
448
|
+
|
|
449
|
+
const renderer = createRenderer({
|
|
450
|
+
router,
|
|
451
|
+
fs,
|
|
452
|
+
stash: appState,
|
|
453
|
+
compile: compileToJS,
|
|
454
|
+
target: '#app',
|
|
455
|
+
transition: { duration: 200 }
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
renderer.start() // Watch for route changes, mount components
|
|
459
|
+
renderer.stop() // Unmount everything, clean up
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Compilation Cache
|
|
463
|
+
|
|
464
|
+
Compiled components are cached by VFS path. A file is only recompiled when it
|
|
465
|
+
changes. The VFS watcher triggers cache invalidation, so updating a file in the
|
|
466
|
+
VFS automatically causes the next render to use the new version.
|
|
467
|
+
|
|
468
|
+
## Quick Start
|
|
469
|
+
|
|
470
|
+
### Minimal HTML Shell
|
|
471
|
+
|
|
472
|
+
```html
|
|
473
|
+
<!DOCTYPE html>
|
|
474
|
+
<html>
|
|
475
|
+
<head><title>My App</title></head>
|
|
476
|
+
<body>
|
|
477
|
+
<div id="app"></div>
|
|
478
|
+
<script type="module">
|
|
479
|
+
import { compileToJS } from './rip.browser.js'
|
|
480
|
+
import { createApp } from './ui.js'
|
|
481
|
+
|
|
482
|
+
const app = createApp({
|
|
483
|
+
target: '#app',
|
|
484
|
+
compile: compileToJS,
|
|
485
|
+
state: { theme: 'light' }
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
// Load pages into the VFS
|
|
489
|
+
await app.load([
|
|
490
|
+
'pages/_layout.rip',
|
|
491
|
+
'pages/index.rip',
|
|
492
|
+
'pages/about.rip'
|
|
493
|
+
])
|
|
494
|
+
|
|
495
|
+
// Start routing and rendering
|
|
496
|
+
app.start()
|
|
497
|
+
</script>
|
|
498
|
+
</body>
|
|
499
|
+
</html>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Inline Components (No Server)
|
|
503
|
+
|
|
504
|
+
```html
|
|
505
|
+
<script type="module">
|
|
506
|
+
import { compileToJS } from './rip.browser.js'
|
|
507
|
+
import { createApp } from './ui.js'
|
|
508
|
+
|
|
509
|
+
createApp({
|
|
510
|
+
target: '#app',
|
|
511
|
+
compile: compileToJS,
|
|
512
|
+
files: {
|
|
513
|
+
'pages/index.rip': `
|
|
514
|
+
Home = component
|
|
515
|
+
render
|
|
516
|
+
h1 "Hello, World"
|
|
517
|
+
p "This was compiled in your browser."
|
|
518
|
+
`
|
|
519
|
+
}
|
|
520
|
+
}).start()
|
|
521
|
+
</script>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### File Structure
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
my-app/
|
|
528
|
+
├── index.html # HTML shell (the only "build" artifact)
|
|
529
|
+
├── rip.browser.js # Rip compiler (40KB)
|
|
530
|
+
├── ui.js # Framework entry point
|
|
531
|
+
├── stash.js # Reactive state
|
|
532
|
+
├── vfs.js # Virtual File System
|
|
533
|
+
├── router.js # File-based router
|
|
534
|
+
├── renderer.js # Component renderer
|
|
535
|
+
├── pages/
|
|
536
|
+
│ ├── _layout.rip # Root layout (nav, footer)
|
|
537
|
+
│ ├── index.rip # Home page → /
|
|
538
|
+
│ ├── about.rip # About page → /about
|
|
539
|
+
│ └── users/
|
|
540
|
+
│ ├── _layout.rip # Users layout → wraps /users/*
|
|
541
|
+
│ ├── index.rip # User list → /users
|
|
542
|
+
│ └── [id].rip # User profile → /users/:id
|
|
543
|
+
└── css/
|
|
544
|
+
└── styles.css # Tailwind or plain CSS
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Render Template Syntax
|
|
548
|
+
|
|
549
|
+
The `render` block uses a concise, indentation-based template syntax:
|
|
550
|
+
|
|
551
|
+
### Tags and Classes
|
|
552
|
+
|
|
553
|
+
```coffee
|
|
554
|
+
render
|
|
555
|
+
div # <div></div>
|
|
556
|
+
div.card # <div class="card"></div>
|
|
557
|
+
div.card.active # <div class="card active"></div>
|
|
558
|
+
button.btn.btn-primary # <button class="btn btn-primary"></button>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Dynamic Classes (CLSX)
|
|
562
|
+
|
|
563
|
+
```coffee
|
|
564
|
+
render
|
|
565
|
+
div.("active" if @selected) # conditional class
|
|
566
|
+
div.("bg-red" if error, "bg-green" if ok) # multiple conditions
|
|
567
|
+
div.card.("highlighted" if @featured) # static + dynamic
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Dynamic class expressions are evaluated at runtime. Falsy values are filtered
|
|
571
|
+
out. This provides native CLSX-like behavior without a library.
|
|
572
|
+
|
|
573
|
+
### Attributes and Events
|
|
574
|
+
|
|
575
|
+
```coffee
|
|
576
|
+
render
|
|
577
|
+
input type: "text", placeholder: "Search..."
|
|
578
|
+
button @click: @handleClick, "Submit"
|
|
579
|
+
a href: "/about", "About Us"
|
|
580
|
+
img src: @imageUrl, alt: "Photo"
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Text and Interpolation
|
|
584
|
+
|
|
585
|
+
```coffee
|
|
586
|
+
render
|
|
587
|
+
h1 "Static text"
|
|
588
|
+
p "Hello, #{@name}"
|
|
589
|
+
span "Count: #{@count}"
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Conditionals
|
|
593
|
+
|
|
594
|
+
```coffee
|
|
595
|
+
render
|
|
596
|
+
if @loggedIn
|
|
597
|
+
p "Welcome back, #{@name}"
|
|
598
|
+
else
|
|
599
|
+
p "Please log in"
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Loops
|
|
603
|
+
|
|
604
|
+
```coffee
|
|
605
|
+
render
|
|
606
|
+
ul
|
|
607
|
+
for item in @items
|
|
608
|
+
li item.name
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Nesting
|
|
612
|
+
|
|
613
|
+
Indentation defines parent-child relationships:
|
|
614
|
+
|
|
615
|
+
```coffee
|
|
616
|
+
render
|
|
617
|
+
div.app
|
|
618
|
+
header.app-header
|
|
619
|
+
h1 "My App"
|
|
620
|
+
nav
|
|
621
|
+
a href: "/", "Home"
|
|
622
|
+
a href: "/about", "About"
|
|
623
|
+
main.app-body
|
|
624
|
+
p "Content goes here"
|
|
625
|
+
footer
|
|
626
|
+
p "Footer"
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## API Reference
|
|
630
|
+
|
|
631
|
+
### `createApp(options)`
|
|
632
|
+
|
|
633
|
+
| Option | Type | Default | Description |
|
|
634
|
+
|--------|------|---------|-------------|
|
|
635
|
+
| `target` | `string\|Element` | `'#app'` | DOM mount target |
|
|
636
|
+
| `state` | `object` | `{}` | Initial app state (becomes reactive stash) |
|
|
637
|
+
| `files` | `object` | `{}` | Initial VFS files `{ path: content }` |
|
|
638
|
+
| `root` | `string` | `'pages'` | Pages directory in VFS |
|
|
639
|
+
| `compile` | `function` | — | Rip compiler (`compileToJS`) |
|
|
640
|
+
| `transition` | `object` | — | Route transition `{ duration }` |
|
|
641
|
+
| `onError` | `function` | — | Error handler |
|
|
642
|
+
| `onNavigate` | `function` | — | Navigation callback |
|
|
643
|
+
|
|
644
|
+
Returns: `{ app, fs, router, renderer, start, stop, load, go, addPage, get, set }`
|
|
645
|
+
|
|
646
|
+
### `stash(data)`
|
|
647
|
+
|
|
648
|
+
Creates a deeply reactive proxy around `data`. Every property read is tracked,
|
|
649
|
+
every write triggers effects.
|
|
650
|
+
|
|
651
|
+
### `effect(fn)`
|
|
652
|
+
|
|
653
|
+
Creates a side effect that re-runs whenever its tracked dependencies change.
|
|
654
|
+
|
|
655
|
+
### `computed(fn)`
|
|
656
|
+
|
|
657
|
+
Creates a lazy computed value that caches until dependencies change.
|
|
658
|
+
|
|
659
|
+
### `batch(fn)`
|
|
660
|
+
|
|
661
|
+
Groups multiple state updates — effects only fire once at the end.
|
|
662
|
+
|
|
663
|
+
## Design Principles
|
|
664
|
+
|
|
665
|
+
**No build step.** The compiler is small enough to ship. Source files are the
|
|
666
|
+
deployment artifact.
|
|
667
|
+
|
|
668
|
+
**Language-native reactivity.** `:=` for state, `~=` for computed, `~>` for
|
|
669
|
+
effects. These are Rip language features, not framework APIs.
|
|
670
|
+
|
|
671
|
+
**Fine-grained DOM updates.** No virtual DOM. Each reactive binding creates a
|
|
672
|
+
direct effect that updates exactly the DOM nodes it touches.
|
|
673
|
+
|
|
674
|
+
**Components are classes.** `component` produces an anonymous ES6 class.
|
|
675
|
+
Methods, lifecycle hooks, and state are ordinary class members. No hooks API, no
|
|
676
|
+
composition functions, no magic — just a class with a `render` method.
|
|
677
|
+
|
|
678
|
+
**Props are instance variables.** `@count := 0` defines a reactive prop. The
|
|
679
|
+
parent can set it, ignore it, or share a signal. The child owns it.
|
|
680
|
+
|
|
681
|
+
**File-based everything.** Components live in the VFS. Routes map to VFS paths.
|
|
682
|
+
Layouts are `_layout.rip` files in the directory tree. The file system is the
|
|
683
|
+
API.
|
|
684
|
+
|
|
685
|
+
## License
|
|
686
|
+
|
|
687
|
+
MIT
|
|
688
|
+
|
|
689
|
+
## Links
|
|
690
|
+
|
|
691
|
+
- [Rip Language](https://github.com/shreeve/rip-lang)
|
|
692
|
+
- [@rip-lang/api](../api/README.md)
|
|
693
|
+
- [@rip-lang/server](../server/README.md)
|
|
694
|
+
- [Report Issues](https://github.com/shreeve/rip-lang/issues)
|