@rip-lang/ui 0.1.3 → 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/package.json
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rip-lang/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-build reactive web framework —
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Zero-build reactive web framework — rip.js + ui.rip + launch(url)",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "ui.
|
|
6
|
+
"main": "ui.rip",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./ui.
|
|
9
|
-
"./stash": "./stash.js",
|
|
10
|
-
"./vfs": "./vfs.js",
|
|
11
|
-
"./router": "./router.js",
|
|
12
|
-
"./renderer": "./renderer.js",
|
|
8
|
+
".": "./ui.rip",
|
|
13
9
|
"./serve": "./serve.rip"
|
|
14
10
|
},
|
|
15
11
|
"scripts": {
|
|
@@ -19,9 +15,6 @@
|
|
|
19
15
|
"ui",
|
|
20
16
|
"framework",
|
|
21
17
|
"reactive",
|
|
22
|
-
"vfs",
|
|
23
|
-
"router",
|
|
24
|
-
"stash",
|
|
25
18
|
"signals",
|
|
26
19
|
"no-build",
|
|
27
20
|
"rip",
|
|
@@ -39,14 +32,10 @@
|
|
|
39
32
|
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
40
33
|
"license": "MIT",
|
|
41
34
|
"dependencies": {
|
|
42
|
-
"rip-lang": "^3.
|
|
35
|
+
"rip-lang": "^3.6.0"
|
|
43
36
|
},
|
|
44
37
|
"files": [
|
|
45
|
-
"ui.
|
|
46
|
-
"stash.js",
|
|
47
|
-
"vfs.js",
|
|
48
|
-
"router.js",
|
|
49
|
-
"renderer.js",
|
|
38
|
+
"ui.rip",
|
|
50
39
|
"serve.rip",
|
|
51
40
|
"README.md"
|
|
52
41
|
],
|
package/serve.rip
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
# @rip-lang/ui/serve — Rip UI Middleware
|
|
2
|
+
# @rip-lang/ui/serve — Rip UI Server Middleware
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
#
|
|
5
|
-
# Serves the Rip UI
|
|
6
|
-
#
|
|
5
|
+
# Serves the Rip UI runtime, auto-generated app bundles, and optional
|
|
6
|
+
# SSE hot-reload.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
9
|
# import { ripUI } from '@rip-lang/ui/serve'
|
|
10
|
-
#
|
|
11
|
-
# use ripUI pages: 'pages', watch: true
|
|
10
|
+
# use ripUI app: '/demo', dir: dir, title: 'My App'
|
|
12
11
|
#
|
|
13
12
|
# Options:
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# GET /pages/* — individual .rip page files (for hot-reload refetch)
|
|
13
|
+
# app: string — URL mount point (default: '')
|
|
14
|
+
# dir: string — app directory on disk (default: '.')
|
|
15
|
+
# components: string — components subdirectory name (default: 'components')
|
|
16
|
+
# watch: boolean — enable SSE hot-reload endpoint (default: false)
|
|
17
|
+
# debounce: number — ms to batch filesystem events (default: 250)
|
|
18
|
+
# state: object — initial app state passed via bundle
|
|
19
|
+
# title: string — document title
|
|
22
20
|
#
|
|
23
21
|
# ==============================================================================
|
|
24
22
|
|
|
@@ -26,61 +24,70 @@ import { get } from '@rip-lang/api'
|
|
|
26
24
|
import { watch as fsWatch } from 'node:fs'
|
|
27
25
|
|
|
28
26
|
export ripUI = (opts = {}) ->
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
prefix = opts.app or ''
|
|
28
|
+
appDir = opts.dir or '.'
|
|
29
|
+
componentsDir = opts.components or "#{appDir}/components"
|
|
30
|
+
enableWatch = opts.watch or false
|
|
31
|
+
debounceMs = opts.debounce or 250
|
|
32
|
+
appState = opts.state or null
|
|
33
|
+
appTitle = opts.title or null
|
|
34
|
+
uiDir = import.meta.dir
|
|
35
|
+
|
|
36
|
+
# Resolve compiler (rip.browser.js)
|
|
36
37
|
compilerPath = null
|
|
37
38
|
try
|
|
38
39
|
compilerPath = Bun.fileURLToPath(import.meta.resolve('rip-lang/docs/dist/rip.browser.js'))
|
|
39
40
|
catch
|
|
40
41
|
compilerPath = "#{uiDir}/../../docs/dist/rip.browser.js"
|
|
41
42
|
|
|
42
|
-
#
|
|
43
|
-
files
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
# Route: /
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Written to file so Bun.file() responses proxy cleanly through rip-server
|
|
72
|
-
if name is 'manifest.json'
|
|
43
|
+
# ----------------------------------------------------------------------------
|
|
44
|
+
# Route: /rip/* — framework files (compiler + ui.rip), registered once
|
|
45
|
+
# ----------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
unless ripUI._registered
|
|
48
|
+
get "/rip/browser.js", (c) -> c.send compilerPath, 'application/javascript'
|
|
49
|
+
get "/rip/ui.rip", (c) -> c.send "#{uiDir}/ui.rip", 'text/plain; charset=UTF-8'
|
|
50
|
+
ripUI._registered = true
|
|
51
|
+
|
|
52
|
+
# ----------------------------------------------------------------------------
|
|
53
|
+
# Route: {prefix}/components/* — individual .rip component files (for hot-reload)
|
|
54
|
+
# Route: {prefix}/bundle — app bundle (components + data as JSON)
|
|
55
|
+
# Route: {prefix}/watch — SSE hot-reload stream
|
|
56
|
+
# ----------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
get "#{prefix}/components/*", (c) ->
|
|
59
|
+
name = c.req.path.slice("#{prefix}/components/".length)
|
|
60
|
+
c.send "#{componentsDir}/#{name}", 'text/plain; charset=UTF-8'
|
|
61
|
+
|
|
62
|
+
bundleCache = null
|
|
63
|
+
bundleDirty = true
|
|
64
|
+
|
|
65
|
+
# Invalidate bundle cache when components change
|
|
66
|
+
if enableWatch
|
|
67
|
+
fsWatch componentsDir, { recursive: true }, (event, filename) ->
|
|
68
|
+
bundleDirty = true if filename?.endsWith('.rip')
|
|
69
|
+
|
|
70
|
+
get "#{prefix}/bundle", (c) ->
|
|
71
|
+
if bundleDirty or not bundleCache
|
|
73
72
|
glob = new Bun.Glob("**/*.rip")
|
|
74
|
-
|
|
75
|
-
paths = Array.from(glob.scanSync(
|
|
73
|
+
components = {}
|
|
74
|
+
paths = Array.from(glob.scanSync(componentsDir))
|
|
76
75
|
for path in paths
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
components["components/#{path}"] = Bun.file("#{componentsDir}/#{path}").text!
|
|
77
|
+
|
|
78
|
+
data = {}
|
|
79
|
+
data.title = appTitle if appTitle
|
|
80
|
+
data.watch = enableWatch
|
|
81
|
+
if appState
|
|
82
|
+
data[k] = v for k, v of appState
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
bundleCache = JSON.stringify({ components, data })
|
|
85
|
+
bundleDirty = false
|
|
86
|
+
|
|
87
|
+
new Response bundleCache, headers: { 'Content-Type': 'application/json' }
|
|
88
|
+
|
|
89
|
+
if enableWatch
|
|
90
|
+
get "#{prefix}/watch", (c) ->
|
|
84
91
|
encoder = new TextEncoder()
|
|
85
92
|
pending = new Set()
|
|
86
93
|
timer = null
|
|
@@ -93,7 +100,7 @@ export ripUI = (opts = {}) ->
|
|
|
93
100
|
clearInterval(heartbeat) if heartbeat
|
|
94
101
|
watcher = heartbeat = timer = null
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
new Response new ReadableStream(
|
|
97
104
|
start: (controller) ->
|
|
98
105
|
send = (event, data) ->
|
|
99
106
|
try
|
|
@@ -101,10 +108,8 @@ export ripUI = (opts = {}) ->
|
|
|
101
108
|
catch
|
|
102
109
|
cleanup()
|
|
103
110
|
|
|
104
|
-
# Send initial connection confirmation
|
|
105
111
|
send 'connected', { time: Date.now() }
|
|
106
112
|
|
|
107
|
-
# Heartbeat every 5s to prevent Bun's idle timeout from closing the SSE
|
|
108
113
|
heartbeat = setInterval ->
|
|
109
114
|
try
|
|
110
115
|
controller.enqueue encoder.encode(": heartbeat\n\n")
|
|
@@ -112,17 +117,15 @@ export ripUI = (opts = {}) ->
|
|
|
112
117
|
cleanup()
|
|
113
118
|
, 5000
|
|
114
119
|
|
|
115
|
-
# Flush pending changes as one batched notification
|
|
116
120
|
flush = ->
|
|
117
121
|
paths = Array.from(pending)
|
|
118
122
|
pending.clear()
|
|
119
123
|
timer = null
|
|
120
124
|
send('changed', { paths }) if paths.length > 0
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
watcher = fsWatch pagesDir, { recursive: true }, (event, filename) ->
|
|
126
|
+
watcher = fsWatch componentsDir, { recursive: true }, (event, filename) ->
|
|
124
127
|
return unless filename?.endsWith('.rip')
|
|
125
|
-
pending.add "
|
|
128
|
+
pending.add "components/#{filename}"
|
|
126
129
|
clearTimeout(timer) if timer
|
|
127
130
|
timer = setTimeout(flush, debounceMs)
|
|
128
131
|
|
|
@@ -133,8 +136,5 @@ export ripUI = (opts = {}) ->
|
|
|
133
136
|
'Cache-Control': 'no-cache'
|
|
134
137
|
'Connection': 'keep-alive'
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
new Response 'Not Found', status: 404
|
|
138
|
-
|
|
139
|
-
# Return pass-through middleware for use()
|
|
139
|
+
# Return pass-through middleware
|
|
140
140
|
(c, next) -> next!()
|