@bitstillery/mithril 3.0.0-AA
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/LICENSE +21 -0
- package/README.md +185 -0
- package/api/mount-redraw.ts +173 -0
- package/api/router-server.ts +88 -0
- package/api/router.ts +670 -0
- package/browser.ts +10 -0
- package/htm.ts +7 -0
- package/hyperscript.ts +8 -0
- package/index.ts +134 -0
- package/jsx.d.ts +16 -0
- package/mount.ts +10 -0
- package/package.json +66 -0
- package/pathname/build.ts +40 -0
- package/pathname/compileTemplate.ts +49 -0
- package/pathname/parse.ts +19 -0
- package/querystring/build.ts +22 -0
- package/querystring/parse.ts +50 -0
- package/redraw.ts +10 -0
- package/render/cachedAttrsIsStaticMap.ts +10 -0
- package/render/delayedRemoval.ts +1 -0
- package/render/domFor.ts +23 -0
- package/render/emptyAttrs.ts +2 -0
- package/render/fragment.ts +11 -0
- package/render/hyperscript.ts +125 -0
- package/render/hyperscriptVnode.ts +21 -0
- package/render/render.ts +1307 -0
- package/render/renderToString.ts +494 -0
- package/render/ssrState.ts +366 -0
- package/render/trust.ts +6 -0
- package/render/vnode.ts +131 -0
- package/route.ts +11 -0
- package/server/logger.ts +223 -0
- package/server/session.ts +87 -0
- package/server/ssr.ts +205 -0
- package/server/ssrLogger.ts +13 -0
- package/server.ts +101 -0
- package/signal.ts +320 -0
- package/ssrContext.ts +87 -0
- package/state.ts +1026 -0
- package/store.ts +457 -0
- package/util/censor.ts +46 -0
- package/util/decodeURIComponentSafe.ts +34 -0
- package/util/hasOwn.ts +2 -0
- package/util/next_tick.ts +45 -0
- package/util/ssr.ts +385 -0
- package/util/uri.ts +126 -0
- package/version.ts +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Leo Horie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @bitstillery/mithril
|
|
2
|
+
|
|
3
|
+
> Mithril.js with **fine-grained reactivity**, **SSR hydration**, and **zero-dependency signals**.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Mithril.js uses global `m.redraw()`—one state change updates **all** components. This fork adds fine-grained reactivity: only components using changed state re-render.
|
|
10
|
+
|
|
11
|
+
## What Makes This Different
|
|
12
|
+
|
|
13
|
+
### Signals
|
|
14
|
+
|
|
15
|
+
Fine-grained reactivity primitives with automatic dependency tracking. Zero-dependency implementation—no Preact Signals or other packages.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import {signal, computed, effect} from '@bitstillery/mithril'
|
|
19
|
+
|
|
20
|
+
const count = signal(0)
|
|
21
|
+
const doubled = computed(() => count() * 2)
|
|
22
|
+
|
|
23
|
+
effect(() => {
|
|
24
|
+
console.log(`Count: ${count()}, Doubled: ${doubled()}`)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
count(5) // Logs: Count: 5, Doubled: 10
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Signal State
|
|
31
|
+
|
|
32
|
+
Proxy-based reactive state that makes signals developer-friendly. Automatic dependency tracking with no manual redraw calls.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import {state} from '@bitstillery/mithril'
|
|
36
|
+
|
|
37
|
+
const $s = state(
|
|
38
|
+
{
|
|
39
|
+
count: 0,
|
|
40
|
+
user: {name: 'John'},
|
|
41
|
+
todos: [],
|
|
42
|
+
totalTodos: () => $s.todos.length, // Computed
|
|
43
|
+
},
|
|
44
|
+
'my.state',
|
|
45
|
+
) // Name required for SSR serialization
|
|
46
|
+
|
|
47
|
+
// Component only re-renders when $s.count changes
|
|
48
|
+
class Counter extends MithrilComponent {
|
|
49
|
+
view() {
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<p>Count: {$s.count}</p>
|
|
53
|
+
<button onclick={() => $s.count++}>Increment</button>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Persistent Store
|
|
61
|
+
|
|
62
|
+
State persistence with automatic serialization. The `Store` class wraps `state()` with localStorage/sessionStorage support, seamlessly integrating with SSR hydration.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import {Store} from '@bitstillery/mithril'
|
|
66
|
+
|
|
67
|
+
const store = new Store<{user: {name: string}; preferences: Record<string, any>}>()
|
|
68
|
+
|
|
69
|
+
// Define what persists vs what's volatile
|
|
70
|
+
store.blueprint(
|
|
71
|
+
{user: {name: ''}, preferences: {}},
|
|
72
|
+
{
|
|
73
|
+
user: {name: ''}, // Persistent
|
|
74
|
+
preferences: {}, // Persistent
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Load from storage, or initialize with defaults
|
|
79
|
+
store.load({user: {name: 'John'}, preferences: {theme: 'dark'}})
|
|
80
|
+
|
|
81
|
+
// State is reactive and automatically saves on changes
|
|
82
|
+
store.state.user.name = 'Jane' // Auto-saves to localStorage
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### SSR Hydration
|
|
86
|
+
|
|
87
|
+
Server-side rendering with state preservation. `renderToString` automatically serializes state; restore it on the client before mounting.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Server
|
|
91
|
+
const {html, state} = await m.renderToString(App)
|
|
92
|
+
// Inject: <script id="__SSR_STATE__">${JSON.stringify(state)}</script>
|
|
93
|
+
|
|
94
|
+
// Client
|
|
95
|
+
import {deserializeAllStates} from '@bitstillery/mithril'
|
|
96
|
+
|
|
97
|
+
const ssrState = document.getElementById('__SSR_STATE__')
|
|
98
|
+
if (ssrState?.textContent) {
|
|
99
|
+
deserializeAllStates(JSON.parse(ssrState.textContent))
|
|
100
|
+
}
|
|
101
|
+
m.mount(root, App)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## The Complete Picture
|
|
105
|
+
|
|
106
|
+
These features build on each other: signals provide the foundation for fine-grained reactivity, proxy-based state makes them developer-friendly, `Store` adds persistence for localStorage/sessionStorage, and SSR hydration enables search-engine friendly websites. The result? State that "just works"—from initial render through hydration, user interactions, and page refreshes—all while maintaining Mithril's familiar API.
|
|
107
|
+
|
|
108
|
+
## Quick Start
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bun add @bitstillery/mithril
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import m, {state, MithrilComponent} from '@bitstillery/mithril'
|
|
116
|
+
|
|
117
|
+
const $s = state({count: 0}, 'app.state') // Name required for SSR
|
|
118
|
+
|
|
119
|
+
class App extends MithrilComponent {
|
|
120
|
+
view() {
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<p>Count: {$s.count}</p>
|
|
124
|
+
<button onclick={() => $s.count++}>Increment</button>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
m.mount(document.body, App)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Examples
|
|
134
|
+
|
|
135
|
+
- **SSR**: [`examples/ssr/`](examples/ssr/) - Server-side rendering with hydration
|
|
136
|
+
- **State**: [`examples/state/`](examples/state/) - Signals, state management, and Store persistence patterns
|
|
137
|
+
|
|
138
|
+
## Differences from Mithril.js
|
|
139
|
+
|
|
140
|
+
| Feature | Original | This Fork |
|
|
141
|
+
| ---------------- | ------------------- | ------------------------------- |
|
|
142
|
+
| Reactivity | Global `m.redraw()` | Fine-grained component updates |
|
|
143
|
+
| State Management | Manual redraw calls | Signals with automatic tracking |
|
|
144
|
+
| SSR Hydration | State loss | Proper state preservation |
|
|
145
|
+
| TypeScript | Community types | Native TypeScript |
|
|
146
|
+
|
|
147
|
+
**100% API compatible** with Mithril.js v2.x. Signals are opt-in.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
bun install
|
|
153
|
+
bun test
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
159
|
+
|
|
160
|
+
## Credits
|
|
161
|
+
|
|
162
|
+
Mithril.js was originally written by Leo Horie, but it is where it is today thanks to the hard work and great ideas of many people.
|
|
163
|
+
|
|
164
|
+
Special thanks to:
|
|
165
|
+
|
|
166
|
+
- Pat Cavit, who exposed most of the public API for Mithril.js 1.0, brought in test coverage and automated the publishing process
|
|
167
|
+
- Claudia Meadows, who brought in linting, modernized the test suite and has been a strong voice in design discussions
|
|
168
|
+
- Zoli Kahan, who replaced the original Promise implementation with one that actually worked properly
|
|
169
|
+
- Alec Embke, who single-handedly wrote the JSON-P implementation
|
|
170
|
+
- Barney Carroll, who suggested many great ideas and relentlessly pushed Mithril.js to the limit to uncover design issues prior to Mithril.js 1.0
|
|
171
|
+
- Dominic Gannaway, who offered insanely meticulous technical insight into rendering performance
|
|
172
|
+
- Boris Letocha, whose search space reduction algorithm is the basis for Mithril.js' virtual DOM engine
|
|
173
|
+
- Joel Richard, whose monomorphic virtual DOM structure is the basis for Mithril.js' vnode implementation
|
|
174
|
+
- Simon Friis Vindum, whose open source work was an inspiration to many design decisions for Mithril.js 1.0
|
|
175
|
+
- Boris Kaul, for his awesome work on the benchmarking tools used to develop Mithril.js
|
|
176
|
+
- Leon Sorokin, for writing a DOM instrumentation tool that helped improve performance in Mithril.js 1.0
|
|
177
|
+
- Jordan Walke, whose work on React was prior art to the implementation of keys in Mithril.js
|
|
178
|
+
- Pierre-Yves Gérardy, who consistently makes high quality contributions
|
|
179
|
+
- Gyandeep Singh, who contributed significant IE performance improvements
|
|
180
|
+
|
|
181
|
+
Other people who also deserve recognition:
|
|
182
|
+
|
|
183
|
+
- Arthur Clemens - creator of [Polythene](https://github.com/ArthurClemens/Polythene) and the [HTML-to-Mithril converter](https://arthurclemens.github.io/mithril-template-converter/index.html)
|
|
184
|
+
- Stephan Hoyer - creator of [mithril-node-render](https://github.com/StephanHoyer/mithril-node-render), [mithril-query](https://github.com/StephanHoyer/mithril-query) and [mithril-source-hint](https://github.com/StephanHoyer/mithril-source-hint)
|
|
185
|
+
- the countless people who have reported and fixed bugs, participated in discussions, and helped promote Mithril.js
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import Vnode from '../render/vnode'
|
|
2
|
+
import {getSignalComponents, type Signal} from '../signal'
|
|
3
|
+
|
|
4
|
+
import type {ComponentType, Children, Vnode as VnodeType} from '../render/vnode'
|
|
5
|
+
|
|
6
|
+
export interface Render {
|
|
7
|
+
(root: Element, vnodes: Children | VnodeType | null, redraw?: () => void): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Redraw {
|
|
11
|
+
(component?: ComponentType): void
|
|
12
|
+
sync(): void
|
|
13
|
+
signal?: (signal: Signal<any>) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Mount {
|
|
17
|
+
(root: Element, component: ComponentType | null): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Schedule {
|
|
21
|
+
(fn: () => void): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Console {
|
|
25
|
+
error: (e: any) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MountRedraw {
|
|
29
|
+
mount: Mount
|
|
30
|
+
redraw: Redraw
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function mountRedrawFactory(render: Render, schedule: Schedule, console: Console): MountRedraw {
|
|
34
|
+
const subscriptions: Array<Element | ComponentType> = []
|
|
35
|
+
const componentToElement = new WeakMap<ComponentType, Element>()
|
|
36
|
+
let pending = false
|
|
37
|
+
let offset = -1
|
|
38
|
+
|
|
39
|
+
function sync() {
|
|
40
|
+
for (offset = 0; offset < subscriptions.length; offset += 2) {
|
|
41
|
+
try {
|
|
42
|
+
render(
|
|
43
|
+
subscriptions[offset] as Element,
|
|
44
|
+
Vnode(subscriptions[offset + 1] as ComponentType, null, null, null, null, null),
|
|
45
|
+
redraw,
|
|
46
|
+
)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(e)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
offset = -1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function redrawComponent(componentOrState: ComponentType) {
|
|
55
|
+
// componentOrState might be vnode.state (from signal tracking) or component object
|
|
56
|
+
// Try to find the actual component object if it's vnode.state
|
|
57
|
+
let component = componentOrState
|
|
58
|
+
const stateToComponentMap = (globalThis as any).__mithrilStateToComponent as WeakMap<any, ComponentType> | undefined
|
|
59
|
+
if (stateToComponentMap && stateToComponentMap.has(componentOrState)) {
|
|
60
|
+
component = stateToComponentMap.get(componentOrState)!
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// First try: find element in componentToElement (for m.mount components)
|
|
64
|
+
// Check this first to ensure synchronous redraws for m.mount components
|
|
65
|
+
const element = componentToElement.get(component)
|
|
66
|
+
if (element) {
|
|
67
|
+
try {
|
|
68
|
+
render(element, Vnode(component, null, null, null, null, null), redraw)
|
|
69
|
+
// If render succeeds, we're done
|
|
70
|
+
return
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(e)
|
|
73
|
+
// If render fails, continue to next check (fall through)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Second try: find DOM element directly from component state (for routed components)
|
|
78
|
+
// Only check this if componentToElement didn't find anything (not an m.mount component)
|
|
79
|
+
const stateToDomMap = (globalThis as any).__mithrilStateToDom as WeakMap<any, Element> | undefined
|
|
80
|
+
if (stateToDomMap && stateToDomMap.has(componentOrState)) {
|
|
81
|
+
// For routed components, always use global redraw to ensure RouterRoot re-renders correctly
|
|
82
|
+
// RouterRoot needs currentResolver and component to be set (from route resolution)
|
|
83
|
+
// A direct redraw might use stale route state, so we trigger a full sync instead
|
|
84
|
+
// This ensures RouterRoot re-renders with the current route, preserving Layout
|
|
85
|
+
if (!pending) {
|
|
86
|
+
pending = true
|
|
87
|
+
schedule(function () {
|
|
88
|
+
pending = false
|
|
89
|
+
sync()
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Third try: find element in subscriptions
|
|
96
|
+
const index = subscriptions.indexOf(component)
|
|
97
|
+
if (index >= 0 && index % 2 === 1) {
|
|
98
|
+
const rootElement = subscriptions[index - 1] as Element
|
|
99
|
+
try {
|
|
100
|
+
render(rootElement, Vnode(component, null, null, null, null, null), redraw)
|
|
101
|
+
// If render succeeds, we're done
|
|
102
|
+
return
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error(e)
|
|
105
|
+
// If render fails, continue to fallback (fall through)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Final fallback: component not found - trigger global redraw
|
|
110
|
+
// This handles edge cases where component tracking failed
|
|
111
|
+
if (!pending) {
|
|
112
|
+
pending = true
|
|
113
|
+
schedule(function () {
|
|
114
|
+
pending = false
|
|
115
|
+
sync()
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function redraw(component?: ComponentType) {
|
|
121
|
+
// Component-level redraw
|
|
122
|
+
if (component !== undefined) {
|
|
123
|
+
redrawComponent(component)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Global redraw (backward compatibility)
|
|
128
|
+
if (!pending) {
|
|
129
|
+
pending = true
|
|
130
|
+
schedule(function () {
|
|
131
|
+
pending = false
|
|
132
|
+
sync()
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
redraw.sync = sync
|
|
138
|
+
|
|
139
|
+
// Export function to redraw components affected by signal changes
|
|
140
|
+
;(redraw as any).signal = function (signal: Signal<any>) {
|
|
141
|
+
const components = getSignalComponents(signal)
|
|
142
|
+
if (components) {
|
|
143
|
+
components.forEach((component) => {
|
|
144
|
+
redrawComponent(component)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function mount(root: Element, component: ComponentType | null) {
|
|
150
|
+
if (component != null && (component as any).view == null && typeof component !== 'function') {
|
|
151
|
+
throw new TypeError('m.mount expects a component, not a vnode.')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const index = subscriptions.indexOf(root)
|
|
155
|
+
if (index >= 0) {
|
|
156
|
+
const oldComponent = subscriptions[index + 1] as ComponentType
|
|
157
|
+
if (oldComponent) {
|
|
158
|
+
componentToElement.delete(oldComponent)
|
|
159
|
+
}
|
|
160
|
+
subscriptions.splice(index, 2)
|
|
161
|
+
if (index <= offset) offset -= 2
|
|
162
|
+
render(root, [])
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (component != null) {
|
|
166
|
+
subscriptions.push(root, component)
|
|
167
|
+
componentToElement.set(component, root)
|
|
168
|
+
render(root, Vnode(component, null, null, null, null, null), redraw)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {mount: mount, redraw: redraw}
|
|
173
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Vnode from '../render/vnode'
|
|
2
|
+
import hyperscript from '../render/hyperscript'
|
|
3
|
+
import decodeURIComponentSafe from '../util/decodeURIComponentSafe'
|
|
4
|
+
import parsePathname from '../pathname/parse'
|
|
5
|
+
import compileTemplate from '../pathname/compileTemplate'
|
|
6
|
+
|
|
7
|
+
import type {RouteResolver} from './router'
|
|
8
|
+
|
|
9
|
+
interface RenderToString {
|
|
10
|
+
(vnodes: any): Promise<string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function routerServerFactory(renderToString: RenderToString) {
|
|
14
|
+
// Compile route templates
|
|
15
|
+
function compileRoutes(routes: Record<string, any>): Array<{
|
|
16
|
+
route: string
|
|
17
|
+
component: any
|
|
18
|
+
check: (data: {path: string; params: Record<string, any>}) => boolean
|
|
19
|
+
}> {
|
|
20
|
+
const compiled: Array<{
|
|
21
|
+
route: string
|
|
22
|
+
component: any
|
|
23
|
+
check: (data: {path: string; params: Record<string, any>}) => boolean
|
|
24
|
+
}> = []
|
|
25
|
+
|
|
26
|
+
for (const route in routes) {
|
|
27
|
+
const component = routes[route]
|
|
28
|
+
const template = compileTemplate(route)
|
|
29
|
+
const check = (data: {path: string; params: Record<string, any>}) => {
|
|
30
|
+
return template(data)
|
|
31
|
+
}
|
|
32
|
+
compiled.push({route, component, check})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return compiled
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Resolve route and render component
|
|
39
|
+
async function resolveRoute(pathname: string, routes: Record<string, any>, prefix: string = ''): Promise<string> {
|
|
40
|
+
const path = decodeURIComponentSafe(pathname).slice(prefix.length)
|
|
41
|
+
const data = parsePathname(path)
|
|
42
|
+
|
|
43
|
+
const compiled = compileRoutes(routes)
|
|
44
|
+
|
|
45
|
+
// Find matching route
|
|
46
|
+
for (const {route, component, check} of compiled) {
|
|
47
|
+
if (check(data)) {
|
|
48
|
+
let payload = component
|
|
49
|
+
|
|
50
|
+
// Handle RouteResolver
|
|
51
|
+
if (payload && typeof payload === 'object' && 'onmatch' in payload) {
|
|
52
|
+
const resolver = payload as RouteResolver
|
|
53
|
+
if (resolver.onmatch) {
|
|
54
|
+
const result = resolver.onmatch(data.params, pathname, route)
|
|
55
|
+
if (result instanceof Promise) {
|
|
56
|
+
payload = await result
|
|
57
|
+
} else if (result !== undefined) {
|
|
58
|
+
payload = result
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If resolver has render, use it
|
|
63
|
+
if (resolver.render) {
|
|
64
|
+
const vnode = Vnode(payload, data.params.key, data.params, null, null, null)
|
|
65
|
+
return await renderToString(resolver.render(vnode))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Render component
|
|
70
|
+
if (payload != null && (typeof payload.view === 'function' || typeof payload === 'function')) {
|
|
71
|
+
const vnode = hyperscript(payload, data.params)
|
|
72
|
+
return await renderToString(vnode)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback to div
|
|
76
|
+
const vnode = hyperscript('div', data.params)
|
|
77
|
+
return await renderToString(vnode)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// No route found
|
|
82
|
+
throw new Error(`No route found for ${pathname}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
resolve: resolveRoute,
|
|
87
|
+
}
|
|
88
|
+
}
|