@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/api/mount-redraw.ts +173 -0
  4. package/api/router-server.ts +88 -0
  5. package/api/router.ts +670 -0
  6. package/browser.ts +10 -0
  7. package/htm.ts +7 -0
  8. package/hyperscript.ts +8 -0
  9. package/index.ts +134 -0
  10. package/jsx.d.ts +16 -0
  11. package/mount.ts +10 -0
  12. package/package.json +66 -0
  13. package/pathname/build.ts +40 -0
  14. package/pathname/compileTemplate.ts +49 -0
  15. package/pathname/parse.ts +19 -0
  16. package/querystring/build.ts +22 -0
  17. package/querystring/parse.ts +50 -0
  18. package/redraw.ts +10 -0
  19. package/render/cachedAttrsIsStaticMap.ts +10 -0
  20. package/render/delayedRemoval.ts +1 -0
  21. package/render/domFor.ts +23 -0
  22. package/render/emptyAttrs.ts +2 -0
  23. package/render/fragment.ts +11 -0
  24. package/render/hyperscript.ts +125 -0
  25. package/render/hyperscriptVnode.ts +21 -0
  26. package/render/render.ts +1307 -0
  27. package/render/renderToString.ts +494 -0
  28. package/render/ssrState.ts +366 -0
  29. package/render/trust.ts +6 -0
  30. package/render/vnode.ts +131 -0
  31. package/route.ts +11 -0
  32. package/server/logger.ts +223 -0
  33. package/server/session.ts +87 -0
  34. package/server/ssr.ts +205 -0
  35. package/server/ssrLogger.ts +13 -0
  36. package/server.ts +101 -0
  37. package/signal.ts +320 -0
  38. package/ssrContext.ts +87 -0
  39. package/state.ts +1026 -0
  40. package/store.ts +457 -0
  41. package/util/censor.ts +46 -0
  42. package/util/decodeURIComponentSafe.ts +34 -0
  43. package/util/hasOwn.ts +2 -0
  44. package/util/next_tick.ts +45 -0
  45. package/util/ssr.ts +385 -0
  46. package/util/uri.ts +126 -0
  47. 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
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ }