@bitstillery/mithril 3.0.0-AA → 3.0.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 +48 -121
- package/index.ts +7 -6
- package/package.json +13 -3
- package/state.ts +4 -1
- package/util/next_tick.ts +4 -5
package/README.md
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
# @bitstillery/mithril
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Mithril.js fork with fine-grained reactivity, SSR hydration, and built-in signals. Drop-in compatible with Mithril v2.x.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
bun add @bitstillery/mithril
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Mithril.js uses global `m.redraw()` — one state change re-renders **everything**. This fork adds component-level reactivity via signals so only affected components update.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
| Feature | Original | This Fork |
|
|
14
|
+
| ---------------- | ------------------- | ------------------------------- |
|
|
15
|
+
| Reactivity | Global `m.redraw()` | Fine-grained component updates |
|
|
16
|
+
| State Management | Manual redraw calls | Signals with automatic tracking |
|
|
17
|
+
| SSR | No hydration | State serialization + hydration |
|
|
18
|
+
| TypeScript | Community types | Native |
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
Signals are opt-in. Existing Mithril code works unchanged.
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
**Docs**: [mithril.garage44.org](https://mithril.garage44.org)
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
## Signals
|
|
25
|
+
|
|
26
|
+
Zero-dependency reactive primitives with automatic dependency tracking:
|
|
16
27
|
|
|
17
28
|
```typescript
|
|
18
29
|
import {signal, computed, effect} from '@bitstillery/mithril'
|
|
@@ -20,71 +31,36 @@ import {signal, computed, effect} from '@bitstillery/mithril'
|
|
|
20
31
|
const count = signal(0)
|
|
21
32
|
const doubled = computed(() => count() * 2)
|
|
22
33
|
|
|
23
|
-
effect(() => {
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
count(5) // Logs: Count: 5, Doubled: 10
|
|
34
|
+
effect(() => console.log(`${count()} × 2 = ${doubled()}`))
|
|
35
|
+
count(5) // Logs: 5 × 2 = 10
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
## Proxy State
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
Wraps signals in a proxy for ergonomic access. Components automatically track which properties they read and only re-render when those change.
|
|
33
41
|
|
|
34
42
|
```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
|
|
43
|
+
import m, {state, MithrilComponent} from '@bitstillery/mithril'
|
|
44
|
+
|
|
45
|
+
const $s = state({count: 0, todos: [], totalTodos: () => $s.todos.length}, 'app')
|
|
46
|
+
|
|
48
47
|
class Counter extends MithrilComponent {
|
|
49
48
|
view() {
|
|
50
49
|
return (
|
|
51
50
|
<div>
|
|
52
|
-
<p>
|
|
53
|
-
<button onclick={() => $s.count++}
|
|
51
|
+
<p>{$s.count}</p>
|
|
52
|
+
<button onclick={() => $s.count++}>+</button>
|
|
54
53
|
</div>
|
|
55
54
|
)
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Persistent Store
|
|
61
57
|
|
|
62
|
-
|
|
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
|
|
58
|
+
m.mount(document.body, Counter)
|
|
83
59
|
```
|
|
84
60
|
|
|
85
|
-
|
|
61
|
+
The second argument to `state()` is a name used for SSR serialization.
|
|
86
62
|
|
|
87
|
-
|
|
63
|
+
## SSR Hydration
|
|
88
64
|
|
|
89
65
|
```typescript
|
|
90
66
|
// Server
|
|
@@ -94,57 +70,31 @@ const {html, state} = await m.renderToString(App)
|
|
|
94
70
|
// Client
|
|
95
71
|
import {deserializeAllStates} from '@bitstillery/mithril'
|
|
96
72
|
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
deserializeAllStates(JSON.parse(ssrState.textContent))
|
|
100
|
-
}
|
|
73
|
+
const el = document.getElementById('__SSR_STATE__')
|
|
74
|
+
if (el?.textContent) deserializeAllStates(JSON.parse(el.textContent))
|
|
101
75
|
m.mount(root, App)
|
|
102
76
|
```
|
|
103
77
|
|
|
104
|
-
##
|
|
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'
|
|
78
|
+
## Persistent Store
|
|
116
79
|
|
|
117
|
-
|
|
80
|
+
`Store` wraps `state()` with localStorage/sessionStorage. Define a blueprint with defaults and which keys persist:
|
|
118
81
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<div>
|
|
123
|
-
<p>Count: {$s.count}</p>
|
|
124
|
-
<button onclick={() => $s.count++}>Increment</button>
|
|
125
|
-
</div>
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
82
|
+
```typescript
|
|
83
|
+
import {Store} from '@bitstillery/mithril'
|
|
129
84
|
|
|
130
|
-
|
|
85
|
+
const store = new Store<{user: {name: string}; preferences: Record<string, any>}>()
|
|
86
|
+
store.blueprint(
|
|
87
|
+
{user: {name: ''}, preferences: {}},
|
|
88
|
+
{user: {name: ''}, preferences: {}}, // Keys here persist to storage
|
|
89
|
+
)
|
|
90
|
+
store.load({user: {name: 'John'}, preferences: {theme: 'dark'}})
|
|
91
|
+
store.state.user.name = 'Jane' // Auto-saves
|
|
131
92
|
```
|
|
132
93
|
|
|
133
94
|
## Examples
|
|
134
95
|
|
|
135
|
-
-
|
|
136
|
-
-
|
|
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.
|
|
96
|
+
- [`examples/ssr/`](examples/ssr/) — Server-side rendering with hydration
|
|
97
|
+
- [`examples/state/`](examples/state/) — Signals, state, and Store patterns
|
|
148
98
|
|
|
149
99
|
## Development
|
|
150
100
|
|
|
@@ -155,31 +105,8 @@ bun test
|
|
|
155
105
|
|
|
156
106
|
## License
|
|
157
107
|
|
|
158
|
-
MIT
|
|
108
|
+
MIT — see [LICENSE](LICENSE).
|
|
159
109
|
|
|
160
110
|
## Credits
|
|
161
111
|
|
|
162
|
-
|
|
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
|
|
112
|
+
Originally created by Leo Horie. See the [Mithril.js contributors](https://github.com/MithrilJS/mithril.js/graphs/contributors) for the many people who made Mithril what it is.
|
package/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './jsx.d.ts'
|
|
1
2
|
import hyperscript from './render/hyperscript'
|
|
2
3
|
import mountRedrawFactory from './api/mount-redraw'
|
|
3
4
|
import routerFactory from './api/router'
|
|
@@ -8,7 +9,7 @@ import parsePathname from './pathname/parse'
|
|
|
8
9
|
import buildPathname from './pathname/build'
|
|
9
10
|
import VnodeFactory, {MithrilComponent} from './render/vnode'
|
|
10
11
|
import censor from './util/censor'
|
|
11
|
-
import
|
|
12
|
+
import nextTick from './util/next_tick'
|
|
12
13
|
import domFor from './render/domFor'
|
|
13
14
|
import {signal, computed, effect, Signal, ComputedSignal, setSignalRedrawCallback, getSignalComponents} from './signal'
|
|
14
15
|
import {state, watch, registerState, getRegisteredStates, copyGlobalStatesToContext} from './state'
|
|
@@ -36,7 +37,7 @@ export interface MithrilStatic {
|
|
|
36
37
|
buildPathname: (template: string, params: Record<string, any>) => string
|
|
37
38
|
vnode: typeof VnodeFactory
|
|
38
39
|
censor: (attrs: Record<string, any>, extras?: string[]) => Record<string, any>
|
|
39
|
-
|
|
40
|
+
nextTick: () => Promise<void>
|
|
40
41
|
domFor: (vnode: Vnode) => Generator<Node, void, unknown>
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -66,7 +67,7 @@ m.parsePathname = parsePathname
|
|
|
66
67
|
m.buildPathname = buildPathname
|
|
67
68
|
m.vnode = VnodeFactory
|
|
68
69
|
m.censor = censor
|
|
69
|
-
m.
|
|
70
|
+
m.nextTick = nextTick
|
|
70
71
|
m.domFor = domFor
|
|
71
72
|
|
|
72
73
|
// Set up signal-to-component redraw integration
|
|
@@ -82,7 +83,7 @@ setSignalRedrawCallback((sig: Signal<any>) => {
|
|
|
82
83
|
|
|
83
84
|
// Export signals API
|
|
84
85
|
export {signal, computed, effect, Signal, ComputedSignal, state, watch, registerState, getRegisteredStates}
|
|
85
|
-
export type {State, StateOptions} from './state'
|
|
86
|
+
export type {State, StateOptions, Unwatch} from './state'
|
|
86
87
|
|
|
87
88
|
// Export Store class
|
|
88
89
|
export {Store} from './store'
|
|
@@ -98,8 +99,8 @@ export type {SSRAccessContext} from './ssrContext'
|
|
|
98
99
|
export {logger, Logger} from './server/logger'
|
|
99
100
|
export type {LogContext} from './server/logger'
|
|
100
101
|
|
|
101
|
-
// Export
|
|
102
|
-
export {
|
|
102
|
+
// Export nextTick utility
|
|
103
|
+
export {nextTick} from './util/next_tick'
|
|
103
104
|
|
|
104
105
|
// Export URI utilities
|
|
105
106
|
export {getCurrentUrl, getPathname, getSearch, getHash, getLocation} from './util/uri'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitstillery/mithril",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Mithril
|
|
3
|
+
"version": "3.0.2",
|
|
4
|
+
"description": "Mithril + Signals, Store and SSR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bitstillery",
|
|
7
7
|
"repository": {
|
|
@@ -58,9 +58,19 @@
|
|
|
58
58
|
"htm": "^3.1.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
+
"@codemirror/lang-html": "^6.4.9",
|
|
62
|
+
"@codemirror/lang-javascript": "^6.2.2",
|
|
63
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
64
|
+
"@codemirror/state": "^6.4.1",
|
|
65
|
+
"@codemirror/theme-one-dark": "^6.1.2",
|
|
66
|
+
"@codemirror/view": "^6.39.1",
|
|
61
67
|
"@types/bun": "^1.3.9",
|
|
62
68
|
"@typescript/native-preview": "^7.0.0-dev.20260120.1",
|
|
69
|
+
"codemirror": "^6.0.1",
|
|
70
|
+
"marked": "^15.0.4",
|
|
71
|
+
"marked-gfm-heading-id": "^4.0.0",
|
|
63
72
|
"oxfmt": "^0.32.0",
|
|
64
|
-
"oxlint": "^1.30.0"
|
|
73
|
+
"oxlint": "^1.30.0",
|
|
74
|
+
"tippy.js": "^6.3.7"
|
|
65
75
|
}
|
|
66
76
|
}
|
package/state.ts
CHANGED
|
@@ -990,13 +990,16 @@ export type State<T extends Record<string, any>> = {
|
|
|
990
990
|
[K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : T[K] extends Record<string, any> ? State<T[K]> : T[K]
|
|
991
991
|
} & StateSignals<T>
|
|
992
992
|
|
|
993
|
+
/** Function returned by watch() to remove the watcher */
|
|
994
|
+
export type Unwatch = () => void
|
|
995
|
+
|
|
993
996
|
/**
|
|
994
997
|
* Watch a signal for changes
|
|
995
998
|
* @param signal - The signal to watch
|
|
996
999
|
* @param callback - Callback function called when signal value changes
|
|
997
1000
|
* @returns Unsubscribe function
|
|
998
1001
|
*/
|
|
999
|
-
export function watch<T>(signal: Signal<T> | ComputedSignal<T>, callback: (newValue: T, oldValue: T) => void):
|
|
1002
|
+
export function watch<T>(signal: Signal<T> | ComputedSignal<T>, callback: (newValue: T, oldValue: T) => void): Unwatch {
|
|
1000
1003
|
const unwatch = signal.watch(callback)
|
|
1001
1004
|
|
|
1002
1005
|
// Register watcher in SSR context for cleanup at end of request
|
package/util/next_tick.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Isomorphic
|
|
2
|
+
* Isomorphic nextTick utility
|
|
3
3
|
*
|
|
4
4
|
* Returns a Promise that resolves after the current execution stack completes.
|
|
5
5
|
*
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @returns Promise that resolves on the next tick
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
async function next_tick(): Promise<void> {
|
|
11
|
+
async function nextTick(): Promise<void> {
|
|
13
12
|
// Check if we're in SSR mode
|
|
14
13
|
if (typeof globalThis !== 'undefined' && (globalThis as any).__SSR_MODE__) {
|
|
15
14
|
// In SSR mode, resolve immediately since SSR rendering is synchronous
|
|
@@ -41,5 +40,5 @@ async function next_tick(): Promise<void> {
|
|
|
41
40
|
return Promise.resolve() as Promise<void>
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
export default
|
|
45
|
-
export {
|
|
43
|
+
export default nextTick
|
|
44
|
+
export {nextTick}
|