@rip-lang/ui 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +223 -40
  2. package/package.json +1 -1
  3. package/serve.rip +5 -8
  4. package/ui.js +1 -1
package/README.md CHANGED
@@ -18,28 +18,43 @@ functions, methods) that Rip already provides.
18
18
  ## Architecture
19
19
 
20
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
21
+ ┌─────────────────────────────────────────┐
22
+ Server (Bun + @rip-lang/api) │
23
+ │ │
24
+ serve.rip (ripUI middleware)
25
+ │ ├── /rip-ui/*.js framework files
26
+ │ ├── /rip-ui/manifest.jsonall 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
34
48
  ```
35
49
 
36
50
  | Module | Size | Role |
37
51
  |--------|------|------|
38
- | `ui.js` | ~150 lines | `createApp` entry point, re-exports everything |
52
+ | `ui.js` | ~175 lines | `createApp` entry point with `loadBundle`, `watch`, re-exports |
39
53
  | `stash.js` | ~400 lines | Deep reactive state tree with path-based navigation |
40
54
  | `vfs.js` | ~200 lines | Browser-local Virtual File System with watchers |
41
55
  | `router.js` | ~300 lines | File-based router (URL ↔ VFS paths, History API) |
42
- | `renderer.js` | ~250 lines | Component lifecycle, layouts, transitions |
56
+ | `renderer.js` | ~250 lines | Component lifecycle, layouts, transitions, `remount` |
57
+ | `serve.rip` | ~140 lines | Server middleware: framework files, manifest, SSE hot-reload |
43
58
 
44
59
  ## The Idea
45
60
 
@@ -70,7 +85,7 @@ with fine-grained DOM manipulation — no virtual DOM diffing.
70
85
  |---|---|---|
71
86
  | **Build step** | Required (Vite, Webpack, etc.) | None — compiler runs in browser |
72
87
  | **Bundle size** | 40-100KB+ framework + app bundle | 40KB compiler + ~8KB framework + raw source |
73
- | **HMR** | Dev server ↔ browser WebSocket | Not needed recompile in-place |
88
+ | **HMR** | Dev server ↔ browser WebSocket | SSE notify + VFS invalidation + recompile |
74
89
  | **Deployment** | Build artifacts (`dist/`) | Source files served as-is |
75
90
  | **Component format** | JSX, SFC, templates | Rip source (`.rip` files) |
76
91
  | **Reactivity** | Library-specific (hooks, refs, signals) | Language-native (`:=`, `~=`, `~>`) |
@@ -465,9 +480,158 @@ Compiled components are cached by VFS path. A file is only recompiled when it
465
480
  changes. The VFS watcher triggers cache invalidation, so updating a file in the
466
481
  VFS automatically causes the next render to use the new version.
467
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) |
511
+
512
+ ### Options
513
+
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:
525
+
526
+ ```json
527
+ {
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": "..."
531
+ }
532
+ ```
533
+
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
538
+
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:
568
+
569
+ ```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
+ });
588
+ ```
589
+
590
+ ### `loadBundle(url)`
591
+
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).
594
+
595
+ ```javascript
596
+ await app.loadBundle('/rip-ui/manifest.json');
597
+ ```
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();
607
+ ```
608
+
468
609
  ## Quick Start
469
610
 
470
- ### Minimal HTML Shell
611
+ ### With Server Integration (Recommended)
612
+
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:
616
+
617
+ **`index.rip`** — The complete server:
618
+
619
+ ```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:
471
635
 
472
636
  ```html
473
637
  <!DOCTYPE html>
@@ -476,30 +640,30 @@ VFS automatically causes the next render to use the new version.
476
640
  <body>
477
641
  <div id="app"></div>
478
642
  <script type="module">
479
- import { compileToJS } from './rip.browser.js'
480
- import { createApp } from './ui.js'
643
+ import { compileToJS } from '/rip-ui/compiler.js';
644
+ import { createApp } from '/rip-ui/ui.js';
481
645
 
482
646
  const app = createApp({
483
647
  target: '#app',
484
648
  compile: compileToJS,
485
649
  state: { theme: 'light' }
486
- })
650
+ });
487
651
 
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()
652
+ await app.loadBundle('/rip-ui/manifest.json');
653
+ app.start();
654
+ app.watch('/rip-ui/watch');
497
655
  </script>
498
656
  </body>
499
657
  </html>
500
658
  ```
501
659
 
502
- ### Inline Components (No Server)
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)
665
+
666
+ For static deployments or quick prototyping, you can inline components directly:
503
667
 
504
668
  ```html
505
669
  <script type="module">
@@ -525,13 +689,8 @@ VFS automatically causes the next render to use the new version.
525
689
 
526
690
  ```
527
691
  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
692
+ ├── index.rip # Server (uses @rip-lang/api + ripUI middleware)
693
+ ├── index.html # HTML shell
535
694
  ├── pages/
536
695
  │ ├── _layout.rip # Root layout (nav, footer)
537
696
  │ ├── index.rip # Home page → /
@@ -544,6 +703,10 @@ my-app/
544
703
  └── styles.css # Tailwind or plain CSS
545
704
  ```
546
705
 
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
+
547
710
  ## Render Template Syntax
548
711
 
549
712
  The `render` block uses a concise, indentation-based template syntax:
@@ -641,7 +804,21 @@ render
641
804
  | `onError` | `function` | — | Error handler |
642
805
  | `onNavigate` | `function` | — | Navigation callback |
643
806
 
644
- Returns: `{ app, fs, router, renderer, start, stop, load, go, addPage, get, set }`
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`
645
822
 
646
823
  ### `stash(data)`
647
824
 
@@ -686,9 +863,15 @@ API.
686
863
 
687
864
  MIT
688
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
+
689
872
  ## Links
690
873
 
691
874
  - [Rip Language](https://github.com/shreeve/rip-lang)
692
- - [@rip-lang/api](../api/README.md)
693
- - [@rip-lang/server](../server/README.md)
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)
694
877
  - [Report Issues](https://github.com/shreeve/rip-lang/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/ui",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Zero-build reactive web framework — VFS, file-based routing, reactive stash",
5
5
  "type": "module",
6
6
  "main": "ui.js",
package/serve.rip CHANGED
@@ -23,7 +23,6 @@
23
23
  # ==============================================================================
24
24
 
25
25
  import { get } from '@rip-lang/api'
26
- import { brotliCompressSync } from 'node:zlib'
27
26
  import { watch as fsWatch } from 'node:fs'
28
27
 
29
28
  export ripUI = (opts = {}) ->
@@ -68,19 +67,17 @@ export ripUI = (opts = {}) ->
68
67
  if files[name]
69
68
  return c.send files[name], 'application/javascript'
70
69
 
71
- # Auto-generated manifest — bundles all .rip sources as JSON (brotli compressed)
70
+ # Auto-generated manifest — bundles all .rip page sources as JSON
71
+ # Written to file so Bun.file() responses proxy cleanly through rip-server
72
72
  if name is 'manifest.json'
73
73
  glob = new Bun.Glob("**/*.rip")
74
74
  bundle = {}
75
75
  paths = Array.from(glob.scanSync(pagesDir))
76
76
  for path in paths
77
77
  bundle["pages/#{path}"] = Bun.file("#{pagesDir}/#{path}").text!
78
- json = JSON.stringify(bundle)
79
- compressed = brotliCompressSync(Buffer.from(json))
80
- return new Response compressed,
81
- headers:
82
- 'Content-Type': 'application/json'
83
- 'Content-Encoding': 'br'
78
+ manifestPath = "#{pagesDir}/.manifest.json"
79
+ Bun.write manifestPath, JSON.stringify(bundle)
80
+ return c.send manifestPath, 'application/json'
84
81
 
85
82
  # SSE watch endpoint — debounced, notify-only, with heartbeat
86
83
  if name is 'watch' and enableWatch
package/ui.js CHANGED
@@ -203,6 +203,6 @@ function defaultErrorHandler({ status, message, path, error }) {
203
203
  // Version
204
204
  // ---------------------------------------------------------------------------
205
205
 
206
- export const VERSION = '0.1.1';
206
+ export const VERSION = '0.1.2';
207
207
 
208
208
  export default createApp;