@rip-lang/ui 0.1.3 → 0.3.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.
package/package.json CHANGED
@@ -1,15 +1,11 @@
1
1
  {
2
2
  "name": "@rip-lang/ui",
3
- "version": "0.1.3",
4
- "description": "Zero-build reactive web framework — VFS, file-based routing, reactive stash",
3
+ "version": "0.3.2",
4
+ "description": "Zero-build reactive web framework — rip.js + ui.rip + launch(url)",
5
5
  "type": "module",
6
- "main": "ui.js",
6
+ "main": "ui.rip",
7
7
  "exports": {
8
- ".": "./ui.js",
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,19 +32,15 @@
39
32
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
40
33
  "license": "MIT",
41
34
  "dependencies": {
42
- "rip-lang": "^3.4.4"
35
+ "rip-lang": "^3.9.1"
43
36
  },
44
37
  "files": [
45
- "ui.js",
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
  ],
53
42
  "peerDependencies": {
54
- "@rip-lang/api": ">=1.1.4"
43
+ "@rip-lang/api": ">=1.1.6"
55
44
  },
56
45
  "peerDependenciesMeta": {
57
46
  "@rip-lang/api": {
package/serve.rip CHANGED
@@ -1,24 +1,23 @@
1
1
  # ==============================================================================
2
- # @rip-lang/ui/serve — Rip UI Middleware for rip-api
2
+ # @rip-lang/ui/serve — Rip UI Server Middleware
3
3
  # ==============================================================================
4
4
  #
5
- # Serves the Rip UI framework files, auto-generated page manifests, and
6
- # provides an SSE hot-reload channel for live development.
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 dir: dir, components: 'pages', includes: ['ui'], watch: true, title: 'My App'
12
11
  #
13
12
  # Options:
14
- # base: string — URL prefix for framework files (default: '/rip-ui')
15
- # pages: string — directory containing .rip page files (default: 'pages')
16
- # watch: booleanenable SSE hot-reload endpoint (default: false)
17
- # debounce: number ms to batch filesystem events (default: 250)
18
- #
19
- # Registered routes:
20
- # GET {base}/* framework JS files, manifest, SSE watch
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 directory for page components, relative to dir (default: 'components')
16
+ # includes: array directories for shared components, relative to dir (default: [])
17
+ # watch: boolean — enable SSE hot-reload endpoint (default: false)
18
+ # debounce: number — ms to batch filesystem events (default: 250)
19
+ # state: object initial app state passed via bundle
20
+ # title: string document title
22
21
  #
23
22
  # ==============================================================================
24
23
 
@@ -26,74 +25,96 @@ import { get } from '@rip-lang/api'
26
25
  import { watch as fsWatch } from 'node:fs'
27
26
 
28
27
  export ripUI = (opts = {}) ->
29
- base = opts.base or '/rip-ui'
30
- pagesDir = opts.pages or 'pages'
31
- enableWatch = opts.watch or false
32
- debounceMs = opts.debounce or 250
33
- uiDir = import.meta.dir
34
-
35
- # Resolve compiler (rip.browser.js) from the rip-lang package
36
- compilerPath = null
28
+ prefix = opts.app or ''
29
+ appDir = opts.dir or '.'
30
+ componentsDir = "#{appDir}/#{opts.components or 'components'}"
31
+ includeDirs = (opts.includes or []).map (d) -> "#{appDir}/#{d}"
32
+ enableWatch = opts.watch or false
33
+ debounceMs = opts.debounce or 250
34
+ appState = opts.state or null
35
+ appTitle = opts.title or null
36
+ uiDir = import.meta.dir
37
+
38
+ # Resolve rip-ui.min.js (compiler + UI framework bundled)
39
+ bundlePath = null
37
40
  try
38
- compilerPath = Bun.fileURLToPath(import.meta.resolve('rip-lang/docs/dist/rip.browser.js'))
41
+ bundlePath = Bun.fileURLToPath(import.meta.resolve('rip-lang/docs/dist/rip-ui.min.js'))
39
42
  catch
40
- compilerPath = "#{uiDir}/../../docs/dist/rip.browser.js"
41
-
42
- # Framework file map: logical name → filesystem path
43
- files =
44
- 'ui.js': "#{uiDir}/ui.js"
45
- 'stash.js': "#{uiDir}/stash.js"
46
- 'vfs.js': "#{uiDir}/vfs.js"
47
- 'router.js': "#{uiDir}/router.js"
48
- 'renderer.js': "#{uiDir}/renderer.js"
49
- 'compiler.js': compilerPath
50
-
51
- # ---------------------------------------------------------------------------
52
- # Route: /pages/* individual .rip page files (for hot-reload refetch)
53
- # ---------------------------------------------------------------------------
54
-
55
- get '/pages/*', (c) ->
56
- name = c.req.path.slice('/pages/'.length)
57
- c.send "#{pagesDir}/#{name}", 'text/plain; charset=UTF-8'
58
-
59
- # ---------------------------------------------------------------------------
60
- # Route: {base}/* — framework files, manifest, SSE watch
61
- # ---------------------------------------------------------------------------
62
-
63
- get "#{base}/*", (c) ->
64
- name = c.req.path.slice(base.length + 1)
65
-
66
- # Framework JS files
67
- if files[name]
68
- return c.send files[name], 'application/javascript'
69
-
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
- if name is 'manifest.json'
43
+ bundlePath = "#{uiDir}/../../docs/dist/rip-ui.min.js"
44
+
45
+ # ----------------------------------------------------------------------------
46
+ # Route: /rip/* — framework files, registered once
47
+ # ----------------------------------------------------------------------------
48
+
49
+ unless ripUI._registered
50
+ get "/rip/rip-ui.min.js", (c) -> c.send bundlePath, 'application/javascript'
51
+ ripUI._registered = true
52
+
53
+ # ----------------------------------------------------------------------------
54
+ # Route: {prefix}/components/* — individual .rip component files (for hot-reload)
55
+ # Route: {prefix}/bundle app bundle (components + data as JSON)
56
+ # Route: {prefix}/watch — SSE hot-reload stream
57
+ # ----------------------------------------------------------------------------
58
+
59
+ get "#{prefix}/components/*", (c) ->
60
+ name = c.req.path.slice("#{prefix}/components/".length)
61
+ c.send "#{componentsDir}/#{name}", 'text/plain; charset=UTF-8'
62
+
63
+ bundleCache = null
64
+ bundleDirty = true
65
+
66
+ # Invalidate bundle cache when components change
67
+ if enableWatch
68
+ fsWatch componentsDir, { recursive: true }, (event, filename) ->
69
+ bundleDirty = true if filename?.endsWith('.rip')
70
+ for incDir in includeDirs
71
+ fsWatch incDir, { recursive: true }, (event, filename) ->
72
+ bundleDirty = true if filename?.endsWith('.rip')
73
+
74
+ get "#{prefix}/bundle", (c) ->
75
+ if bundleDirty or not bundleCache
73
76
  glob = new Bun.Glob("**/*.rip")
74
- bundle = {}
75
- paths = Array.from(glob.scanSync(pagesDir))
77
+ components = {}
78
+ paths = Array.from(glob.scanSync(componentsDir))
76
79
  for path in paths
77
- bundle["pages/#{path}"] = Bun.file("#{pagesDir}/#{path}").text!
78
- manifestPath = "#{pagesDir}/.manifest.json"
79
- Bun.write manifestPath, JSON.stringify(bundle)
80
- return c.send manifestPath, 'application/json'
80
+ components["components/#{path}"] = Bun.file("#{componentsDir}/#{path}").text!
81
+
82
+ # Merge external include directories into components/_lib/
83
+ for dir in includeDirs
84
+ incPaths = Array.from(glob.scanSync(dir))
85
+ for path in incPaths
86
+ key = "components/_lib/#{path}"
87
+ components[key] = Bun.file("#{dir}/#{path}").text! unless components[key]
88
+
89
+ data = {}
90
+ data.title = appTitle if appTitle
91
+ data.watch = enableWatch
92
+ if appState
93
+ data[k] = v for k, v of appState
94
+
95
+ bundleCache = JSON.stringify({ components, data })
96
+ bundleDirty = false
81
97
 
82
- # SSE watch endpoint debounced, notify-only, with heartbeat
83
- if name is 'watch' and enableWatch
98
+ new Response bundleCache, headers: { 'Content-Type': 'application/json' }
99
+
100
+ if enableWatch
101
+ get "#{prefix}/watch", (c) ->
84
102
  encoder = new TextEncoder()
85
103
  pending = new Set()
86
104
  timer = null
87
105
  watcher = null
88
106
  heartbeat = null
89
107
 
108
+ watchers = []
109
+
90
110
  cleanup = ->
91
- watcher?.close()
111
+ w.close() for w in watchers
92
112
  clearTimeout(timer) if timer
93
113
  clearInterval(heartbeat) if heartbeat
94
- watcher = heartbeat = timer = null
114
+ watchers = []
115
+ heartbeat = timer = null
95
116
 
96
- return new Response new ReadableStream(
117
+ new Response new ReadableStream(
97
118
  start: (controller) ->
98
119
  send = (event, data) ->
99
120
  try
@@ -101,10 +122,8 @@ export ripUI = (opts = {}) ->
101
122
  catch
102
123
  cleanup()
103
124
 
104
- # Send initial connection confirmation
105
125
  send 'connected', { time: Date.now() }
106
126
 
107
- # Heartbeat every 5s to prevent Bun's idle timeout from closing the SSE
108
127
  heartbeat = setInterval ->
109
128
  try
110
129
  controller.enqueue encoder.encode(": heartbeat\n\n")
@@ -112,19 +131,22 @@ export ripUI = (opts = {}) ->
112
131
  cleanup()
113
132
  , 5000
114
133
 
115
- # Flush pending changes as one batched notification
116
134
  flush = ->
117
135
  paths = Array.from(pending)
118
136
  pending.clear()
119
137
  timer = null
120
138
  send('changed', { paths }) if paths.length > 0
121
139
 
122
- # Watch the pages directory for .rip file changes
123
- watcher = fsWatch pagesDir, { recursive: true }, (event, filename) ->
124
- return unless filename?.endsWith('.rip')
125
- pending.add "pages/#{filename}"
126
- clearTimeout(timer) if timer
127
- timer = setTimeout(flush, debounceMs)
140
+ addWatcher = (dir, keyPrefix) ->
141
+ watchers.push fsWatch dir, { recursive: true }, (event, filename) ->
142
+ return unless filename?.endsWith('.rip')
143
+ pending.add "#{keyPrefix}#{filename}"
144
+ clearTimeout(timer) if timer
145
+ timer = setTimeout(flush, debounceMs)
146
+
147
+ addWatcher componentsDir, 'components/'
148
+ for incDir in includeDirs
149
+ addWatcher incDir, 'components/_lib/'
128
150
 
129
151
  cancel: -> cleanup()
130
152
  ),
@@ -133,8 +155,5 @@ export ripUI = (opts = {}) ->
133
155
  'Cache-Control': 'no-cache'
134
156
  'Connection': 'keep-alive'
135
157
 
136
- # Unknown path under /rip-ui/
137
- new Response 'Not Found', status: 404
138
-
139
- # Return pass-through middleware for use()
158
+ # Return pass-through middleware
140
159
  (c, next) -> next!()