@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
@@ -0,0 +1,366 @@
1
+ import {ComputedSignal} from '../signal'
2
+ import {getRegisteredStates} from '../state'
3
+ import {logger} from '../server/ssrLogger'
4
+
5
+ import type {State} from '../state'
6
+
7
+ /**
8
+ * Check if a value is a state (has __isState flag)
9
+ */
10
+ function isState(value: any): boolean {
11
+ return value && typeof value === 'object' && (value as any).__isState === true
12
+ }
13
+
14
+ /**
15
+ * Serialize a single state to plain object
16
+ * Extracts signal values by accessing state properties directly
17
+ */
18
+ export function serializeStore(state: State<any>): any {
19
+ if (!isState(state)) {
20
+ throw new Error('Value is not a state')
21
+ }
22
+
23
+ const result: Record<string, any> = {}
24
+ const visited = new WeakSet<object>()
25
+
26
+ // Add the root state to visited first to detect circular refs
27
+ visited.add(state)
28
+
29
+ // Check if signalMap exists and is a Map - if not, this is an error case
30
+ const signalMap = (state as any).__signalMap
31
+ if (!signalMap || !(signalMap instanceof Map)) {
32
+ // Throw error so serializeAllStates can catch it and skip this state
33
+ throw new Error('State signalMap is null, undefined, or not a Map instance')
34
+ }
35
+
36
+ function serializeValue(value: any): any {
37
+ // Handle null/undefined
38
+ if (value === null || value === undefined) {
39
+ return value
40
+ }
41
+
42
+ // Handle primitives
43
+ if (typeof value !== 'object') {
44
+ return value
45
+ }
46
+
47
+ // Handle circular references
48
+ // Check before adding to visited to avoid false positives
49
+ if (visited.has(value)) {
50
+ return null // Circular reference - serialize as null
51
+ }
52
+
53
+ // Mark as visited BEFORE processing to detect circular refs
54
+ visited.add(value)
55
+
56
+ // Handle states (nested states and arrays)
57
+ if (isState(value)) {
58
+ // Check if this is an array state
59
+ const signals = (value as any).__signals
60
+ if (signals && Array.isArray(signals)) {
61
+ // Array state - serialize each element
62
+ const arrayResult = signals
63
+ .map((signal: any) => {
64
+ if (signal instanceof ComputedSignal) {
65
+ return undefined
66
+ }
67
+ // Get the actual value from the signal
68
+ // If it's a Signal instance, get its value; otherwise it's already the value (could be a state)
69
+ let sigValue: any
70
+ // Check if it's a Signal instance (has value property and is an instance of Signal/ComputedSignal)
71
+ if (signal && typeof signal === 'object' && 'value' in signal && !isState(signal)) {
72
+ sigValue = signal.value
73
+ } else {
74
+ // It's either a primitive or a state (nested object)
75
+ sigValue = signal
76
+ }
77
+ // If sigValue is a state (nested object in array), serialize it
78
+ // This will use __originalKeys to filter parent keys
79
+ const serialized = serializeValue(sigValue)
80
+ // If circular reference was detected, it returns null - keep it
81
+ return serialized
82
+ })
83
+ .filter((item: any) => item !== undefined)
84
+ return arrayResult
85
+ }
86
+
87
+ // Object state - serialize by accessing properties directly through proxy
88
+ // Use originalKeys to filter out parent state keys
89
+ const nestedResult: Record<string, any> = {}
90
+ const nestedOriginalKeys = (value as any).__originalKeys as Set<string> | undefined
91
+
92
+ for (const key in value) {
93
+ if (
94
+ key.startsWith('$') ||
95
+ key === '__isState' ||
96
+ key === '__signalMap' ||
97
+ key === '__signals' ||
98
+ key === '__originalKeys'
99
+ ) {
100
+ continue
101
+ }
102
+
103
+ // For nested states, only serialize keys that belong to this state (not parent keys)
104
+ if (nestedOriginalKeys && !nestedOriginalKeys.has(key)) {
105
+ continue
106
+ }
107
+
108
+ try {
109
+ // Check if this is a ComputedSignal by accessing $key
110
+ const signal = (value as any)['$' + key]
111
+ if (signal instanceof ComputedSignal) {
112
+ // Skip computed signals
113
+ continue
114
+ }
115
+
116
+ const propValue = (value as any)[key]
117
+ const serialized = serializeValue(propValue)
118
+ // Include null values (they might be circular ref markers)
119
+ nestedResult[key] = serialized
120
+ } catch {
121
+ // Skip if access fails
122
+ continue
123
+ }
124
+ }
125
+ // Don't delete from visited here - it will be cleaned up when we finish serializing this value
126
+ return nestedResult
127
+ }
128
+
129
+ // Handle plain arrays
130
+ if (Array.isArray(value)) {
131
+ const arrayResult = value.map((item) => serializeValue(item))
132
+ return arrayResult
133
+ }
134
+
135
+ // Handle plain objects
136
+ const objResult: Record<string, any> = {}
137
+ for (const key in value) {
138
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
139
+ const serialized = serializeValue(value[key])
140
+ // Include null values (they might be circular ref markers)
141
+ objResult[key] = serialized
142
+ }
143
+ }
144
+ // Don't delete from visited - let it be cleaned up naturally
145
+ return objResult
146
+ }
147
+
148
+ // Serialize by iterating over state properties directly
149
+ // For top-level state, serialize all keys (including dynamically added ones)
150
+ // Use $ prefix to check if property is a ComputedSignal
151
+ for (const key in state) {
152
+ if (
153
+ key.startsWith('$') ||
154
+ key === '__isState' ||
155
+ key === '__signalMap' ||
156
+ key === '__signals' ||
157
+ key === '__originalKeys'
158
+ ) {
159
+ continue
160
+ }
161
+
162
+ try {
163
+ // Check if this is a ComputedSignal by accessing $key
164
+ const signal = (state as any)['$' + key]
165
+ if (signal instanceof ComputedSignal) {
166
+ // Skip computed signals (they're functions, recreated on client)
167
+ continue
168
+ }
169
+
170
+ const value = (state as any)[key]
171
+ result[key] = serializeValue(value)
172
+ } catch {
173
+ // Skip if access fails
174
+ continue
175
+ }
176
+ }
177
+
178
+ return result
179
+ }
180
+
181
+ /**
182
+ * Deserialize state into a state
183
+ * Restores signal values from serialized data
184
+ */
185
+ export function deserializeStore(state: State<any>, serialized: any): void {
186
+ if (!isState(state)) {
187
+ throw new Error('Value is not a state')
188
+ }
189
+
190
+ if (!serialized || typeof serialized !== 'object') {
191
+ return
192
+ }
193
+
194
+ const signalMap = (state as any).__signalMap as Map<string, any>
195
+ if (!signalMap || !(signalMap instanceof Map)) {
196
+ // Throw error so deserializeAllStates can catch it and skip this state
197
+ throw new Error('State signalMap is null, undefined, or not a Map instance')
198
+ }
199
+
200
+ function deserializeValue(value: any): any {
201
+ // Handle null/undefined
202
+ if (value === null || value === undefined) {
203
+ return value
204
+ }
205
+
206
+ // Handle primitives
207
+ if (typeof value !== 'object') {
208
+ return value
209
+ }
210
+
211
+ // Handle arrays
212
+ if (Array.isArray(value)) {
213
+ return value.map((item) => deserializeValue(item))
214
+ }
215
+
216
+ // Handle plain objects (could be nested states)
217
+ // Check if this looks like a serialized state (has state-like structure)
218
+ // For now, treat all objects as plain objects - nested states will be handled
219
+ // when they're assigned to state properties
220
+ const objResult: Record<string, any> = {}
221
+ for (const key in value) {
222
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
223
+ objResult[key] = deserializeValue(value[key])
224
+ }
225
+ }
226
+ return objResult
227
+ }
228
+
229
+ // Deserialize each key in serialized data
230
+ for (const key in serialized) {
231
+ if (Object.prototype.hasOwnProperty.call(serialized, key)) {
232
+ const serializedValue = serialized[key]
233
+ const deserializedValue = deserializeValue(serializedValue)
234
+
235
+ // Check if signal exists in state
236
+ if (signalMap && signalMap.has(key)) {
237
+ const signal = signalMap.get(key)
238
+ // Don't update ComputedSignal (they're read-only)
239
+ if (signal && !(signal instanceof ComputedSignal)) {
240
+ signal.value = deserializedValue
241
+ }
242
+ } else {
243
+ // Signal doesn't exist - use proxy setter to create it
244
+ // But only if signalMap exists (error case: signalMap is null)
245
+ // If signalMap is null, we can't create new signals, so skip
246
+ if (signalMap) {
247
+ ;(state as any)[key] = deserializedValue
248
+ }
249
+ // If signalMap is null, skip this key (error case)
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Serialize all registered states
257
+ * Returns a Record mapping state names to serialized state
258
+ */
259
+ export function serializeAllStates(): Record<string, any> {
260
+ const registeredStates = getRegisteredStates()
261
+ const result: Record<string, any> = {}
262
+
263
+ for (const [name, entry] of registeredStates.entries()) {
264
+ try {
265
+ result[name] = serializeStore(entry.state)
266
+ } catch (error) {
267
+ // Log error but continue with other states
268
+ logger.error(`error serializing state`, error, {stateName: name})
269
+ }
270
+ }
271
+
272
+ return result
273
+ }
274
+
275
+ /**
276
+ * Deserialize all states from serialized data
277
+ * Restores state into registered states
278
+ */
279
+ /**
280
+ * Restore computed properties from initial state
281
+ * Extracts function properties and sets them on state instance
282
+ * State proxy will automatically convert them to ComputedSignal instances
283
+ */
284
+ function restoreComputedProperties(state: State<any>, initial: any): void {
285
+ if (!initial || typeof initial !== 'object') {
286
+ return
287
+ }
288
+
289
+ function isObject(v: any): boolean {
290
+ return v && typeof v === 'object' && !Array.isArray(v)
291
+ }
292
+
293
+ function restore(obj: any, target: any, prefix: string = ''): void {
294
+ for (const key in obj) {
295
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
296
+ const value = obj[key]
297
+
298
+ if (typeof value === 'function') {
299
+ // Set function property - state proxy will convert to ComputedSignal
300
+ const keys = prefix ? prefix.split('.').filter((k) => k) : []
301
+ let targetState = target
302
+ for (let i = 0; i < keys.length; i++) {
303
+ if (!targetState[keys[i]]) {
304
+ // Nested state doesn't exist yet, skip
305
+ return
306
+ }
307
+ targetState = targetState[keys[i]]
308
+ }
309
+ // Clear any existing signal in signalMap so function is re-initialized as ComputedSignal
310
+ if (targetState && typeof targetState === 'object' && (targetState as any).__isState) {
311
+ const signalMap = (targetState as any).__signalMap
312
+ if (signalMap && signalMap instanceof Map) {
313
+ signalMap.delete(key)
314
+ }
315
+ }
316
+ targetState[key] = value
317
+ } else if (isObject(value)) {
318
+ // Recursively restore nested computed properties
319
+ const nestedPrefix = prefix ? `${prefix}.${key}` : key
320
+ restore(value, target, nestedPrefix)
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ restore(initial, state)
327
+ }
328
+
329
+ export function deserializeAllStates(serialized: Record<string, any>): void {
330
+ if (!serialized || typeof serialized !== 'object') {
331
+ return
332
+ }
333
+
334
+ const registeredStates = getRegisteredStates()
335
+
336
+ // First, deserialize all states
337
+ for (const [name, serializedState] of Object.entries(serialized)) {
338
+ const entry = registeredStates.get(name)
339
+
340
+ if (!entry) {
341
+ // State not registered on client - warn in development
342
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
343
+ logger.warn(`state not found in registry. skipping deserialization.`, {stateName: name})
344
+ }
345
+ continue
346
+ }
347
+
348
+ try {
349
+ deserializeStore(entry.state, serializedState)
350
+ } catch (error) {
351
+ // Log error but continue with other states
352
+ logger.error(`error deserializing state`, error, {stateName: name})
353
+ }
354
+ }
355
+
356
+ // After deserializing, restore computed properties from original initial states
357
+ // This ensures computed properties work after SSR deserialization
358
+ for (const [name, entry] of registeredStates.entries()) {
359
+ try {
360
+ restoreComputedProperties(entry.state, entry.initial)
361
+ } catch (error) {
362
+ // Log error but continue with other states
363
+ logger.error(`error restoring computed properties for state`, error, {stateName: name})
364
+ }
365
+ }
366
+ }
@@ -0,0 +1,6 @@
1
+ import Vnode from './vnode'
2
+
3
+ export default function trust(html: string | null | undefined): any {
4
+ if (html == null) html = ''
5
+ return Vnode('<', undefined, undefined, html, undefined, undefined)
6
+ }
@@ -0,0 +1,131 @@
1
+ // Type definitions for Mithril components and vnodes
2
+
3
+ export interface Vnode<Attrs = Record<string, any>, State = any> {
4
+ tag: string | Component<Attrs, State> | (() => Component<Attrs, State>)
5
+ key?: string | number | null
6
+ attrs?: Attrs
7
+ children?: Children
8
+ text?: string | number
9
+ dom?: Node | null
10
+ is?: string
11
+ domSize?: number
12
+ state?: State
13
+ events?: Record<string, any>
14
+ instance?: any
15
+ }
16
+
17
+ /** Single child - Vnode/Element, primitives, or null/undefined */
18
+ export type Child = Vnode | string | number | boolean | null | undefined
19
+ /** Children: single child, array of children, or primitives. Arrays may contain false (conditionally hidden). */
20
+ export type Children = Child | Child[]
21
+
22
+ /** Vnode with dom guaranteed (oncreate/onupdate lifecycle) */
23
+ export type VnodeDOM<Attrs = Record<string, any>, State = any> = ComponentVnode<Attrs, State> & {dom: Element}
24
+
25
+ /**
26
+ * Vnode passed to component lifecycle methods - attrs is always defined (Mithril passes at least {}).
27
+ * Use this so vnode.attrs is never undefined in view/oninit/oncreate etc.
28
+ */
29
+ export type ComponentVnode<Attrs = Record<string, any>, State = any> = Omit<Vnode<Attrs, State>, 'attrs'> & {attrs: Attrs}
30
+
31
+ export interface Component<Attrs = Record<string, any>, State = any> {
32
+ oninit?: (vnode: ComponentVnode<Attrs, State>) => void
33
+ oncreate?: (vnode: ComponentVnode<Attrs, State>) => void
34
+ onbeforeupdate?: (vnode: ComponentVnode<Attrs, State>, old: ComponentVnode<Attrs, State>) => boolean | void
35
+ onupdate?: (vnode: ComponentVnode<Attrs, State>) => void
36
+ onbeforeremove?: (vnode: ComponentVnode<Attrs, State>) => Promise<any> | void
37
+ onremove?: (vnode: ComponentVnode<Attrs, State>) => void
38
+ view: (vnode: ComponentVnode<Attrs, State>) => Children | Vnode | null
39
+ }
40
+
41
+ export interface ComponentFactory<Attrs = Record<string, any>, State = any> {
42
+ (...args: any[]): Component<Attrs, State>
43
+ view?: (vnode: ComponentVnode<Attrs, State>) => Children | Vnode | null
44
+ }
45
+
46
+ export type ComponentType<Attrs = Record<string, any>, State = any> =
47
+ | Component<Attrs, State>
48
+ | ComponentFactory<Attrs, State>
49
+ | (() => Component<Attrs, State>)
50
+ | (new (...args: any[]) => MithrilComponent<Attrs>)
51
+
52
+ /**
53
+ * Abstract base class for TSX/JSX class-based components.
54
+ * Assign view as a property so TypeScript infers vnode from the template: view = (vnode) => { ... }
55
+ */
56
+ export abstract class MithrilComponent<Attrs = Record<string, any>> {
57
+ /** Required for JSX attribute type-checking - do not use directly */
58
+ private readonly __tsx_attrs!: (unknown extends Attrs ? Record<string, any> : Attrs) & {key?: string | number}
59
+
60
+ oninit?(vnode: ComponentVnode<Attrs>): void
61
+ oncreate?(vnode: ComponentVnode<Attrs>): void
62
+ onbeforeupdate?(vnode: ComponentVnode<Attrs>, old: ComponentVnode<Attrs>): boolean | void
63
+ onupdate?(vnode: ComponentVnode<Attrs>): void
64
+ onbeforeremove?(vnode: ComponentVnode<Attrs>): Promise<any> | void
65
+ onremove?(vnode: ComponentVnode<Attrs>): void
66
+ /** Implement in subclass: view(vnode) { ... } - annotate vnode as m.Vnode<Attrs> */
67
+ abstract view(vnode: ComponentVnode<Attrs>): Children | Vnode | null
68
+ }
69
+
70
+ /** Helper type for Vnode of a component - use when this['Vnode'] is not available */
71
+ export type VnodeOf<T> = T extends MithrilComponent<infer A> ? ComponentVnode<A> : never
72
+
73
+ function Vnode(
74
+ tag: any,
75
+ key: string | number | null | undefined,
76
+ attrs: Record<string, any> | null | undefined,
77
+ children: Children | null | undefined,
78
+ text: string | number | null | undefined,
79
+ dom: Node | null | undefined,
80
+ ): Vnode {
81
+ return {
82
+ tag: tag,
83
+ key: key ?? undefined,
84
+ attrs: attrs ?? undefined,
85
+ children: children ?? undefined,
86
+ text: text ?? undefined,
87
+ dom: dom ?? undefined,
88
+ is: undefined,
89
+ domSize: undefined,
90
+ state: undefined,
91
+ events: undefined,
92
+ instance: undefined,
93
+ }
94
+ }
95
+ const normalize = function (node: any): Vnode | null {
96
+ if (Array.isArray(node)) return Vnode('[', undefined, undefined, normalizeChildren(node) as Children, undefined, undefined)
97
+ if (node == null || typeof node === 'boolean') return null
98
+ if (typeof node === 'object') return node
99
+ return Vnode('#', undefined, undefined, String(node), undefined, undefined)
100
+ }
101
+
102
+ const normalizeChildren = function (input: any[]): (Vnode | null)[] {
103
+ // Preallocate the array length (initially holey) and fill every index immediately in order.
104
+ // Benchmarking shows better performance on V8.
105
+ const children = Array.from({length: input.length}) as (Vnode | null)[]
106
+ // Count the number of keyed normalized vnodes for consistency check.
107
+ // Note: this is a perf-sensitive check.
108
+ // Fun fact: merging the loop like this is somehow faster than splitting
109
+ // the check within updateNodes(), noticeably so.
110
+ let numKeyed = 0
111
+ for (let i = 0; i < input.length; i++) {
112
+ children[i] = normalize(input[i])
113
+ if (children[i] !== null && children[i]!.key != null) numKeyed++
114
+ }
115
+ if (numKeyed !== 0 && numKeyed !== input.length) {
116
+ throw new TypeError(
117
+ children.includes(null)
118
+ ? 'In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole.'
119
+ : 'In fragments, vnodes must either all have keys or none have keys.',
120
+ )
121
+ }
122
+ return children
123
+ }
124
+
125
+ ;(Vnode as any).normalize = normalize
126
+ ;(Vnode as any).normalizeChildren = normalizeChildren
127
+
128
+ export default Vnode as typeof Vnode & {
129
+ normalize: typeof normalize
130
+ normalizeChildren: typeof normalizeChildren
131
+ }
package/route.ts ADDED
@@ -0,0 +1,11 @@
1
+ import mountRedraw from './api/mount-redraw'
2
+ import routerFactory from './api/router'
3
+ import renderFactory from './render/render'
4
+
5
+ const mountRedrawInstance = mountRedraw(
6
+ renderFactory(),
7
+ typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame.bind(window) : setTimeout,
8
+ console,
9
+ )
10
+
11
+ export default routerFactory(typeof window !== 'undefined' ? window : null, mountRedrawInstance)