@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
|
@@ -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
|
+
}
|
package/render/trust.ts
ADDED
package/render/vnode.ts
ADDED
|
@@ -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)
|