@rip-lang/ui 0.3.0 → 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/README.md +133 -25
- package/package.json +3 -3
- package/serve.rip +36 -17
- package/ui.rip +40 -11
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ import { get, use, start, notFound } from '@rip-lang/api'
|
|
|
16
16
|
import { ripUI } from '@rip-lang/ui/serve'
|
|
17
17
|
|
|
18
18
|
dir = import.meta.dir
|
|
19
|
-
use ripUI dir: dir, watch: true, title: 'My App'
|
|
19
|
+
use ripUI dir: dir, components: 'pages', includes: ['ui'], watch: true, title: 'My App'
|
|
20
20
|
get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
|
|
21
21
|
notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
|
|
22
22
|
start port: 3000
|
|
@@ -25,47 +25,101 @@ start port: 3000
|
|
|
25
25
|
**`index.html`** — the page:
|
|
26
26
|
|
|
27
27
|
```html
|
|
28
|
-
<script type="module" src="/rip/
|
|
28
|
+
<script type="module" src="/rip/rip-ui.min.js"></script>
|
|
29
29
|
<script type="text/rip">
|
|
30
|
-
{ launch } = importRip! '
|
|
30
|
+
{ launch } = importRip! 'ui.rip'
|
|
31
31
|
launch()
|
|
32
32
|
</script>
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
**`
|
|
35
|
+
**`pages/index.rip`** — a page component:
|
|
36
36
|
|
|
37
37
|
```coffee
|
|
38
38
|
export Home = component
|
|
39
39
|
@count := 0
|
|
40
40
|
render
|
|
41
|
-
|
|
41
|
+
.
|
|
42
42
|
h1 "Hello from Rip UI"
|
|
43
43
|
button @click: (-> @count += 1), "Clicked #{@count} times"
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Run `bun index.rip`, open `http://localhost:3000`.
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## Component Composition
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Page components in `pages/` map to routes via file-based routing. Shared
|
|
51
|
+
components in `ui/` (or any `includes` directory) are available by PascalCase
|
|
52
|
+
name. No imports needed:
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
```coffee
|
|
55
|
+
# ui/card.rip
|
|
56
|
+
export Card = component
|
|
57
|
+
title =! ""
|
|
58
|
+
render
|
|
59
|
+
.card
|
|
60
|
+
if title
|
|
61
|
+
h3 "#{title}"
|
|
62
|
+
@children
|
|
63
|
+
|
|
64
|
+
# pages/about.rip
|
|
65
|
+
export About = component
|
|
66
|
+
render
|
|
67
|
+
.
|
|
68
|
+
h1 "About"
|
|
69
|
+
Card title: "The Idea"
|
|
70
|
+
p "Components compose naturally."
|
|
71
|
+
Card title: "Architecture"
|
|
72
|
+
p "PascalCase resolution, signal passthrough, children blocks."
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Reactive props via `:=` signal passthrough. Readonly props via `=!`.
|
|
76
|
+
Children blocks passed as DOM nodes via `@children`.
|
|
77
|
+
|
|
78
|
+
## How It Works
|
|
79
|
+
|
|
80
|
+
The browser loads one file — `rip-ui.min.js` (~52KB Brotli) — which bundles the
|
|
81
|
+
Rip compiler and the pre-compiled UI framework. No runtime compilation of the
|
|
82
|
+
framework, no extra network requests.
|
|
54
83
|
|
|
55
84
|
Then `launch()` fetches the app bundle, hydrates the stash, and renders.
|
|
56
85
|
|
|
86
|
+
### Browser Execution Contexts
|
|
87
|
+
|
|
88
|
+
Rip provides full async/await support across every browser context — no other
|
|
89
|
+
compile-to-JS language has this:
|
|
90
|
+
|
|
91
|
+
| Context | How async works | Returns value? |
|
|
92
|
+
|---------|-----------------|----------------|
|
|
93
|
+
| `<script type="text/rip">` | Async IIFE wrapper | No (fire-and-forget) |
|
|
94
|
+
| Playground "Run" button | Async IIFE wrapper | No (use console.log) |
|
|
95
|
+
| `rip()` console REPL | Rip `do ->` block | Yes (sync direct, async via Promise) |
|
|
96
|
+
| `.rip` files via `importRip()` | ES module import | Yes (module exports) |
|
|
97
|
+
|
|
98
|
+
The `!` postfix compiles to `await`. Inline scripts are wrapped in an async IIFE
|
|
99
|
+
automatically. The `rip()` console function wraps user code in a `do ->` block
|
|
100
|
+
so the Rip compiler handles implicit return and auto-async natively.
|
|
101
|
+
|
|
102
|
+
### globalThis Exports
|
|
103
|
+
|
|
104
|
+
When `rip-ui.min.js` loads, it registers these on `globalThis`:
|
|
105
|
+
|
|
106
|
+
| Function | Purpose |
|
|
107
|
+
|----------|---------|
|
|
108
|
+
| `rip(code)` | Console REPL — compile and execute Rip code |
|
|
109
|
+
| `importRip(url)` | Fetch, compile, and import a `.rip` file as an ES module |
|
|
110
|
+
| `compileToJS(code)` | Compile Rip source to JavaScript |
|
|
111
|
+
| `__rip` | Reactive runtime — `__state`, `__computed`, `__effect`, `__batch` |
|
|
112
|
+
| `__ripComponent` | Component runtime — `__Component`, `__clsx`, `__fragment` |
|
|
113
|
+
| `__ripExports` | All compiler exports — `compile`, `formatSExpr`, `VERSION`, etc. |
|
|
114
|
+
|
|
57
115
|
## The Stash
|
|
58
116
|
|
|
59
|
-
|
|
117
|
+
App state lives in one reactive tree:
|
|
60
118
|
|
|
61
119
|
```
|
|
62
120
|
app
|
|
63
|
-
├──
|
|
64
|
-
|
|
65
|
-
│ ├── counter.rip
|
|
66
|
-
│ └── _layout.rip
|
|
67
|
-
├── routes ← navigation state (path, params, query, hash)
|
|
68
|
-
└── data ← reactive app state (title, theme, user, etc.)
|
|
121
|
+
├── routes ← navigation state (path, params, query, hash)
|
|
122
|
+
└── data ← reactive app state (title, theme, user, etc.)
|
|
69
123
|
```
|
|
70
124
|
|
|
71
125
|
Writing to `app.data.theme` updates any component reading it. The stash
|
|
@@ -74,13 +128,14 @@ uses Rip's built-in reactive primitives — the same signals that power
|
|
|
74
128
|
|
|
75
129
|
## The App Bundle
|
|
76
130
|
|
|
77
|
-
The bundle is JSON served at `/{app}/bundle
|
|
131
|
+
The bundle is JSON served at `/{app}/bundle`:
|
|
78
132
|
|
|
79
133
|
```json
|
|
80
134
|
{
|
|
81
135
|
"components": {
|
|
82
136
|
"components/index.rip": "export Home = component...",
|
|
83
|
-
"components/counter.rip": "export Counter = component..."
|
|
137
|
+
"components/counter.rip": "export Counter = component...",
|
|
138
|
+
"components/_lib/card.rip": "export Card = component..."
|
|
84
139
|
},
|
|
85
140
|
"data": {
|
|
86
141
|
"title": "My App",
|
|
@@ -89,20 +144,26 @@ The bundle is JSON served at `/{app}/bundle`. It populates the stash:
|
|
|
89
144
|
}
|
|
90
145
|
```
|
|
91
146
|
|
|
147
|
+
On disk you organize your app into `pages/` and `ui/`. The middleware
|
|
148
|
+
maps them into a flat `components/` namespace in the bundle — pages go
|
|
149
|
+
under `components/`, shared components under `components/_lib/`. The `_`
|
|
150
|
+
prefix tells the router to skip `_lib/` entries when generating routes.
|
|
151
|
+
|
|
92
152
|
## Server Middleware
|
|
93
153
|
|
|
94
154
|
The `ripUI` middleware registers routes for the framework files, the app
|
|
95
155
|
bundle, and optional SSE hot-reload:
|
|
96
156
|
|
|
97
157
|
```coffee
|
|
98
|
-
use ripUI
|
|
158
|
+
use ripUI dir: dir, components: 'pages', includes: ['ui'], watch: true, title: 'My App'
|
|
99
159
|
```
|
|
100
160
|
|
|
101
161
|
| Option | Default | Description |
|
|
102
162
|
|--------|---------|-------------|
|
|
103
163
|
| `app` | `''` | URL mount point |
|
|
104
164
|
| `dir` | `'.'` | App directory on disk |
|
|
105
|
-
| `components` | `'components'` |
|
|
165
|
+
| `components` | `'components'` | Directory for page components (file-based routing) |
|
|
166
|
+
| `includes` | `[]` | Directories for shared components (no routes) |
|
|
106
167
|
| `watch` | `false` | Enable SSE hot-reload |
|
|
107
168
|
| `debounce` | `250` | Milliseconds to batch file change events |
|
|
108
169
|
| `state` | `null` | Initial app state |
|
|
@@ -111,11 +172,10 @@ use ripUI app: '/demo', dir: dir, title: 'My App'
|
|
|
111
172
|
Routes registered:
|
|
112
173
|
|
|
113
174
|
```
|
|
114
|
-
/rip/
|
|
115
|
-
/rip/ui.rip — UI framework
|
|
175
|
+
/rip/rip-ui.min.js — Rip compiler + pre-compiled UI framework
|
|
116
176
|
/{app}/bundle — app bundle (components + data as JSON)
|
|
117
177
|
/{app}/watch — SSE hot-reload stream (when watch: true)
|
|
118
|
-
/{app}/components/*
|
|
178
|
+
/{app}/components/* — individual component files (for hot-reload refetch)
|
|
119
179
|
```
|
|
120
180
|
|
|
121
181
|
## State Preservation (Keep-Alive)
|
|
@@ -184,7 +244,6 @@ get '/', -> Response.redirect('/demo/', 302)
|
|
|
184
244
|
start port: 3002
|
|
185
245
|
```
|
|
186
246
|
|
|
187
|
-
Each app is a directory with `components/`, `css/`, `index.html`, and `index.rip`.
|
|
188
247
|
The `/rip/` namespace is shared — all apps use the same compiler and framework.
|
|
189
248
|
|
|
190
249
|
## File Structure
|
|
@@ -193,16 +252,65 @@ The `/rip/` namespace is shared — all apps use the same compiler and framework
|
|
|
193
252
|
my-app/
|
|
194
253
|
├── index.rip # Server
|
|
195
254
|
├── index.html # HTML page
|
|
196
|
-
├── components
|
|
255
|
+
├── pages/ # Page components (file-based routing)
|
|
197
256
|
│ ├── _layout.rip # Root layout
|
|
198
257
|
│ ├── index.rip # Home → /
|
|
199
258
|
│ ├── about.rip # About → /about
|
|
200
259
|
│ └── users/
|
|
201
260
|
│ └── [id].rip # User profile → /users/:id
|
|
261
|
+
├── ui/ # Shared components (no routes)
|
|
262
|
+
│ └── card.rip # Card → available as Card
|
|
202
263
|
└── css/
|
|
203
264
|
└── styles.css # Styles
|
|
204
265
|
```
|
|
205
266
|
|
|
267
|
+
Files starting with `_` don't generate routes (`_layout.rip` is a layout,
|
|
268
|
+
not a page). Directories starting with `_` are also excluded, which is how
|
|
269
|
+
shared components from `includes` stay out of the router.
|
|
270
|
+
|
|
271
|
+
## Hash Routing
|
|
272
|
+
|
|
273
|
+
For static hosting (GitHub Pages, S3, etc.) where the server can't handle
|
|
274
|
+
SPA fallback routing, use hash-based URLs:
|
|
275
|
+
|
|
276
|
+
```coffee
|
|
277
|
+
launch '/app', hash: true
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This switches from `/about` to `page.html#/about`. Back/forward navigation,
|
|
281
|
+
direct URL loading, and `href="#/path"` links all work correctly.
|
|
282
|
+
|
|
283
|
+
## Static Deployment — `launch bundle:`
|
|
284
|
+
|
|
285
|
+
Inline all components in a single HTML file for zero-server deployment.
|
|
286
|
+
Use `rip-ui.min.js` (~52KB Brotli) — a combined bundle with the compiler
|
|
287
|
+
and pre-compiled UI framework. No extra network requests, no runtime
|
|
288
|
+
compilation of the framework:
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
<script type="module" src="dist/rip-ui.min.js"></script>
|
|
292
|
+
<script type="text/rip">
|
|
293
|
+
{ launch } = importRip! 'ui.rip'
|
|
294
|
+
|
|
295
|
+
launch bundle:
|
|
296
|
+
'/': '''
|
|
297
|
+
export Home = component
|
|
298
|
+
render
|
|
299
|
+
h1 "Hello"
|
|
300
|
+
'''
|
|
301
|
+
'/about': '''
|
|
302
|
+
export About = component
|
|
303
|
+
render
|
|
304
|
+
h1 "About"
|
|
305
|
+
'''
|
|
306
|
+
, hash: true
|
|
307
|
+
</script>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See `docs/demo.html` for a complete example — the full Rip UI Demo app
|
|
311
|
+
(6 components, router, reactive state, persistence) in 337 lines of
|
|
312
|
+
static HTML.
|
|
313
|
+
|
|
206
314
|
## License
|
|
207
315
|
|
|
208
316
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rip-lang/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Zero-build reactive web framework — rip.js + ui.rip + launch(url)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ui.rip",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"rip-lang": "^3.
|
|
35
|
+
"rip-lang": "^3.9.1"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"ui.rip",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"README.md"
|
|
41
41
|
],
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@rip-lang/api": ">=1.1.
|
|
43
|
+
"@rip-lang/api": ">=1.1.6"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"@rip-lang/api": {
|
package/serve.rip
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
9
|
# import { ripUI } from '@rip-lang/ui/serve'
|
|
10
|
-
# use ripUI
|
|
10
|
+
# use ripUI dir: dir, components: 'pages', includes: ['ui'], watch: true, title: 'My App'
|
|
11
11
|
#
|
|
12
12
|
# Options:
|
|
13
13
|
# app: string — URL mount point (default: '')
|
|
14
14
|
# dir: string — app directory on disk (default: '.')
|
|
15
|
-
# components: string — components
|
|
15
|
+
# components: string — directory for page components, relative to dir (default: 'components')
|
|
16
|
+
# includes: array — directories for shared components, relative to dir (default: [])
|
|
16
17
|
# watch: boolean — enable SSE hot-reload endpoint (default: false)
|
|
17
18
|
# debounce: number — ms to batch filesystem events (default: 250)
|
|
18
19
|
# state: object — initial app state passed via bundle
|
|
@@ -26,27 +27,27 @@ import { watch as fsWatch } from 'node:fs'
|
|
|
26
27
|
export ripUI = (opts = {}) ->
|
|
27
28
|
prefix = opts.app or ''
|
|
28
29
|
appDir = opts.dir or '.'
|
|
29
|
-
componentsDir = opts.components or
|
|
30
|
+
componentsDir = "#{appDir}/#{opts.components or 'components'}"
|
|
31
|
+
includeDirs = (opts.includes or []).map (d) -> "#{appDir}/#{d}"
|
|
30
32
|
enableWatch = opts.watch or false
|
|
31
33
|
debounceMs = opts.debounce or 250
|
|
32
34
|
appState = opts.state or null
|
|
33
35
|
appTitle = opts.title or null
|
|
34
36
|
uiDir = import.meta.dir
|
|
35
37
|
|
|
36
|
-
# Resolve
|
|
37
|
-
|
|
38
|
+
# Resolve rip-ui.min.js (compiler + UI framework bundled)
|
|
39
|
+
bundlePath = null
|
|
38
40
|
try
|
|
39
|
-
|
|
41
|
+
bundlePath = Bun.fileURLToPath(import.meta.resolve('rip-lang/docs/dist/rip-ui.min.js'))
|
|
40
42
|
catch
|
|
41
|
-
|
|
43
|
+
bundlePath = "#{uiDir}/../../docs/dist/rip-ui.min.js"
|
|
42
44
|
|
|
43
45
|
# ----------------------------------------------------------------------------
|
|
44
|
-
# Route: /rip/* — framework files
|
|
46
|
+
# Route: /rip/* — framework files, registered once
|
|
45
47
|
# ----------------------------------------------------------------------------
|
|
46
48
|
|
|
47
49
|
unless ripUI._registered
|
|
48
|
-
get "/rip/
|
|
49
|
-
get "/rip/ui.rip", (c) -> c.send "#{uiDir}/ui.rip", 'text/plain; charset=UTF-8'
|
|
50
|
+
get "/rip/rip-ui.min.js", (c) -> c.send bundlePath, 'application/javascript'
|
|
50
51
|
ripUI._registered = true
|
|
51
52
|
|
|
52
53
|
# ----------------------------------------------------------------------------
|
|
@@ -66,6 +67,9 @@ export ripUI = (opts = {}) ->
|
|
|
66
67
|
if enableWatch
|
|
67
68
|
fsWatch componentsDir, { recursive: true }, (event, filename) ->
|
|
68
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')
|
|
69
73
|
|
|
70
74
|
get "#{prefix}/bundle", (c) ->
|
|
71
75
|
if bundleDirty or not bundleCache
|
|
@@ -75,6 +79,13 @@ export ripUI = (opts = {}) ->
|
|
|
75
79
|
for path in paths
|
|
76
80
|
components["components/#{path}"] = Bun.file("#{componentsDir}/#{path}").text!
|
|
77
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
|
+
|
|
78
89
|
data = {}
|
|
79
90
|
data.title = appTitle if appTitle
|
|
80
91
|
data.watch = enableWatch
|
|
@@ -94,11 +105,14 @@ export ripUI = (opts = {}) ->
|
|
|
94
105
|
watcher = null
|
|
95
106
|
heartbeat = null
|
|
96
107
|
|
|
108
|
+
watchers = []
|
|
109
|
+
|
|
97
110
|
cleanup = ->
|
|
98
|
-
|
|
111
|
+
w.close() for w in watchers
|
|
99
112
|
clearTimeout(timer) if timer
|
|
100
113
|
clearInterval(heartbeat) if heartbeat
|
|
101
|
-
|
|
114
|
+
watchers = []
|
|
115
|
+
heartbeat = timer = null
|
|
102
116
|
|
|
103
117
|
new Response new ReadableStream(
|
|
104
118
|
start: (controller) ->
|
|
@@ -123,11 +137,16 @@ export ripUI = (opts = {}) ->
|
|
|
123
137
|
timer = null
|
|
124
138
|
send('changed', { paths }) if paths.length > 0
|
|
125
139
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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/'
|
|
131
150
|
|
|
132
151
|
cancel: -> cleanup()
|
|
133
152
|
),
|
package/ui.rip
CHANGED
|
@@ -360,6 +360,10 @@ buildRoutes = (components, root = 'components') ->
|
|
|
360
360
|
|
|
361
361
|
continue if name.startsWith('_')
|
|
362
362
|
|
|
363
|
+
# Skip files in _-prefixed directories (shared components, not pages)
|
|
364
|
+
segs = rel.split('/')
|
|
365
|
+
continue if segs.length > 1 and segs.some((s, i) -> i < segs.length - 1 and s.startsWith('_'))
|
|
366
|
+
|
|
363
367
|
urlPattern = fileToPattern(rel)
|
|
364
368
|
regex = patternToRegex(urlPattern)
|
|
365
369
|
routes.push { pattern: urlPattern, regex, file: filePath, rel }
|
|
@@ -392,6 +396,7 @@ getLayoutChain = (routeFile, root, layouts) ->
|
|
|
392
396
|
export createRouter = (components, opts = {}) ->
|
|
393
397
|
root = opts.root or 'components'
|
|
394
398
|
base = opts.base or ''
|
|
399
|
+
hashMode = opts.hash or false
|
|
395
400
|
onError = opts.onError or null
|
|
396
401
|
|
|
397
402
|
stripBase = (url) ->
|
|
@@ -400,7 +405,21 @@ export createRouter = (components, opts = {}) ->
|
|
|
400
405
|
addBase = (path) ->
|
|
401
406
|
if base then base + path else path
|
|
402
407
|
|
|
403
|
-
|
|
408
|
+
readUrl = ->
|
|
409
|
+
if hashMode
|
|
410
|
+
h = location.hash.slice(1)
|
|
411
|
+
return '/' unless h
|
|
412
|
+
if h[0] is '/' then h else '/' + h
|
|
413
|
+
else
|
|
414
|
+
location.pathname + location.search + location.hash
|
|
415
|
+
|
|
416
|
+
writeUrl = (path) ->
|
|
417
|
+
if hashMode
|
|
418
|
+
if path is '/' then location.pathname else '#' + path.slice(1)
|
|
419
|
+
else
|
|
420
|
+
addBase(path)
|
|
421
|
+
|
|
422
|
+
_path = __state(stripBase(if hashMode then readUrl() else location.pathname))
|
|
404
423
|
_params = __state({})
|
|
405
424
|
_route = __state(null)
|
|
406
425
|
_layouts = __state([])
|
|
@@ -418,6 +437,7 @@ export createRouter = (components, opts = {}) ->
|
|
|
418
437
|
resolve = (url) ->
|
|
419
438
|
rawPath = url.split('?')[0].split('#')[0]
|
|
420
439
|
path = stripBase(rawPath)
|
|
440
|
+
path = if path[0] is '/' then path else '/' + path
|
|
421
441
|
queryStr = url.split('?')[1]?.split('#')[0] or ''
|
|
422
442
|
hash = if url.includes('#') then url.split('#')[1] else ''
|
|
423
443
|
|
|
@@ -436,7 +456,7 @@ export createRouter = (components, opts = {}) ->
|
|
|
436
456
|
onError({ status: 404, path }) if onError
|
|
437
457
|
false
|
|
438
458
|
|
|
439
|
-
onPopState = -> resolve(
|
|
459
|
+
onPopState = -> resolve(readUrl())
|
|
440
460
|
window.addEventListener 'popstate', onPopState if typeof window isnt 'undefined'
|
|
441
461
|
|
|
442
462
|
onClick = (e) ->
|
|
@@ -448,18 +468,19 @@ export createRouter = (components, opts = {}) ->
|
|
|
448
468
|
return if url.origin isnt location.origin
|
|
449
469
|
return if target.target is '_blank' or target.hasAttribute('data-external')
|
|
450
470
|
e.preventDefault()
|
|
451
|
-
|
|
471
|
+
dest = if hashMode and url.hash then (url.hash.slice(1) or '/') else (url.pathname + url.search + url.hash)
|
|
472
|
+
router.push dest
|
|
452
473
|
|
|
453
474
|
document.addEventListener 'click', onClick if typeof document isnt 'undefined'
|
|
454
475
|
|
|
455
476
|
router =
|
|
456
477
|
push: (url) ->
|
|
457
478
|
if resolve(url)
|
|
458
|
-
history.pushState null, '',
|
|
479
|
+
history.pushState null, '', writeUrl(_path.read())
|
|
459
480
|
|
|
460
481
|
replace: (url) ->
|
|
461
482
|
if resolve(url)
|
|
462
|
-
history.replaceState null, '',
|
|
483
|
+
history.replaceState null, '', writeUrl(_path.read())
|
|
463
484
|
|
|
464
485
|
back: -> history.back()
|
|
465
486
|
forward: -> history.forward()
|
|
@@ -482,7 +503,7 @@ export createRouter = (components, opts = {}) ->
|
|
|
482
503
|
routes: undefined # overridden by getter
|
|
483
504
|
|
|
484
505
|
init: ->
|
|
485
|
-
resolve
|
|
506
|
+
resolve readUrl()
|
|
486
507
|
router
|
|
487
508
|
|
|
488
509
|
destroy: ->
|
|
@@ -794,11 +815,14 @@ export createRenderer = (opts = {}) ->
|
|
|
794
815
|
# ==============================================================================
|
|
795
816
|
|
|
796
817
|
export launch = (appBase = '', opts = {}) ->
|
|
818
|
+
if typeof appBase is 'object'
|
|
819
|
+
opts = appBase
|
|
820
|
+
appBase = ''
|
|
797
821
|
appBase = appBase.replace(/\/+$/, '') # strip trailing slashes
|
|
798
822
|
target = opts.target or '#app'
|
|
799
823
|
compile = opts.compile or null
|
|
800
824
|
persist = opts.persist or false
|
|
801
|
-
|
|
825
|
+
hash = opts.hash or false
|
|
802
826
|
|
|
803
827
|
# Auto-detect compile function from the global rip.js module
|
|
804
828
|
unless compile
|
|
@@ -810,10 +834,14 @@ export launch = (appBase = '', opts = {}) ->
|
|
|
810
834
|
el.id = target.replace(/^#/, '')
|
|
811
835
|
document.body.prepend el
|
|
812
836
|
|
|
813
|
-
#
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
837
|
+
# Get the app bundle — inline, from DOM, or fetch from server
|
|
838
|
+
if opts.bundle
|
|
839
|
+
bundle = opts.bundle
|
|
840
|
+
else
|
|
841
|
+
bundleUrl = "#{appBase}/bundle"
|
|
842
|
+
res = await fetch(bundleUrl)
|
|
843
|
+
throw new Error "launch: #{bundleUrl} (#{res.status})" unless res.ok
|
|
844
|
+
bundle = res.json!
|
|
817
845
|
|
|
818
846
|
# Create the unified stash
|
|
819
847
|
app = stash { components: {}, routes: {}, data: {} }
|
|
@@ -860,6 +888,7 @@ export launch = (appBase = '', opts = {}) ->
|
|
|
860
888
|
router = createRouter appComponents,
|
|
861
889
|
root: 'components'
|
|
862
890
|
base: appBase
|
|
891
|
+
hash: hash
|
|
863
892
|
onError: (err) -> console.error "[Rip] Error #{err.status}: #{err.message or err.path}"
|
|
864
893
|
|
|
865
894
|
# Create renderer
|