@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/util/ssr.ts ADDED
@@ -0,0 +1,385 @@
1
+ // SSR and hydration utilities
2
+
3
+ import {logger} from '../server/logger'
4
+
5
+ // Development-only hydration debugging
6
+ export const HYDRATION_DEBUG = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
7
+
8
+ // Throttle hydration error logging to avoid performance issues
9
+ let hydrationErrorCount = 0
10
+ const MAX_HYDRATION_ERRORS = 10 // Limit number of errors logged per render cycle
11
+
12
+ // Reset error count at the start of each render cycle
13
+ export function resetHydrationErrorCount(): void {
14
+ hydrationErrorCount = 0
15
+ }
16
+
17
+ export function getComponentName(vnode: any): string {
18
+ if (!vnode) return 'Unknown'
19
+ if (typeof vnode.tag === 'string') return vnode.tag
20
+ if (vnode.tag?.name) return vnode.tag.name
21
+ if (vnode.tag?.displayName) return vnode.tag.displayName
22
+ if (vnode.state?.constructor?.name) return vnode.state.constructor.name
23
+ return 'Unknown'
24
+ }
25
+
26
+ // Format a DOM element as an opening tag string
27
+ function formatDOMElement(el: Element): {tagName: string; openTag: string; closeTag: string} {
28
+ const tagName = el.tagName.toLowerCase()
29
+ let openTag = `<${tagName}`
30
+
31
+ // Add important attributes
32
+ if (el.id) {
33
+ openTag += ` id="${el.id}"`
34
+ }
35
+ if (el.className && typeof el.className === 'string') {
36
+ const classes = el.className
37
+ .split(' ')
38
+ .filter((c) => c)
39
+ .slice(0, 3)
40
+ .join(' ')
41
+ if (classes) {
42
+ openTag += ` className="${classes}${el.className.split(' ').length > 3 ? '...' : ''}"`
43
+ }
44
+ }
45
+
46
+ openTag += '>'
47
+ return {tagName, openTag, closeTag: `</${tagName}>`}
48
+ }
49
+
50
+ export function formatVDOMTree(
51
+ vnode: any,
52
+ maxDepth: number = 6,
53
+ currentDepth: number = 0,
54
+ showComponentInstance: boolean = true,
55
+ ): string {
56
+ if (!vnode || currentDepth >= maxDepth) return ''
57
+
58
+ const indent = ' '.repeat(currentDepth)
59
+
60
+ // Handle text nodes
61
+ if (vnode.tag === '#') {
62
+ const text = String(vnode.children || vnode.text || '').substring(0, 50)
63
+ return `${indent}"${text}${String(vnode.children || vnode.text || '').length > 50 ? '...' : ''}"`
64
+ }
65
+
66
+ // Handle fragments
67
+ if (vnode.tag === '[') {
68
+ if (!vnode.children || !Array.isArray(vnode.children) || vnode.children.length === 0) {
69
+ return `${indent}[fragment]`
70
+ }
71
+ const validChildren = vnode.children.filter((c: any) => c != null).slice(0, 8)
72
+ let result = `${indent}[fragment]\n`
73
+ for (const child of validChildren) {
74
+ result += formatVDOMTree(child, maxDepth, currentDepth + 1, showComponentInstance) + '\n'
75
+ }
76
+ if (vnode.children.filter((c: any) => c != null).length > 8) {
77
+ result += `${indent} ... (${vnode.children.filter((c: any) => c != null).length - 8} more)\n`
78
+ }
79
+ return result.trimEnd()
80
+ }
81
+
82
+ const isComponent = typeof vnode.tag !== 'string'
83
+ const tagName = isComponent ? getComponentName(vnode) : vnode.tag
84
+
85
+ let result = `${indent}<${tagName}`
86
+
87
+ // Add key if present
88
+ if (vnode.attrs?.key) {
89
+ result += ` key="${vnode.attrs.key}"`
90
+ }
91
+
92
+ // Add a few important attributes for debugging
93
+ if (vnode.attrs) {
94
+ const importantAttrs = ['id', 'class', 'className']
95
+ for (const attr of importantAttrs) {
96
+ if (vnode.attrs[attr]) {
97
+ const value = typeof vnode.attrs[attr] === 'string' ? vnode.attrs[attr] : String(vnode.attrs[attr])
98
+ result += ` ${attr}="${value.substring(0, 30)}${value.length > 30 ? '...' : ''}"`
99
+ break // Only show first important attr to keep it concise
100
+ }
101
+ }
102
+ }
103
+
104
+ result += '>'
105
+
106
+ // For components, show their rendered instance (what the component actually renders)
107
+ // This gives us parent context without performance cost
108
+ if (isComponent && showComponentInstance && vnode.instance && currentDepth < maxDepth - 1) {
109
+ const instanceTree = formatVDOMTree(vnode.instance, maxDepth, currentDepth + 1, showComponentInstance)
110
+ if (instanceTree) {
111
+ result += '\n' + instanceTree
112
+ }
113
+ }
114
+
115
+ // Add children
116
+ if (vnode.children && Array.isArray(vnode.children) && currentDepth < maxDepth - 1) {
117
+ const validChildren = vnode.children.filter((c: any) => c != null).slice(0, 10)
118
+ if (validChildren.length > 0) {
119
+ result += '\n'
120
+ for (const child of validChildren) {
121
+ if (typeof child === 'string' || typeof child === 'number') {
122
+ const text = String(child).substring(0, 50)
123
+ result += `${indent} "${text}${String(child).length > 50 ? '...' : ''}"\n`
124
+ } else {
125
+ const childTree = formatVDOMTree(child, maxDepth, currentDepth + 1, showComponentInstance)
126
+ if (childTree) {
127
+ result += childTree + '\n'
128
+ }
129
+ }
130
+ }
131
+ if (vnode.children.filter((c: any) => c != null).length > 10) {
132
+ result += `${indent} ... (${vnode.children.filter((c: any) => c != null).length - 10} more children)\n`
133
+ }
134
+ }
135
+ } else if (vnode.text != null) {
136
+ const text = String(vnode.text).substring(0, 50)
137
+ result += ` "${text}${String(vnode.text).length > 50 ? '...' : ''}"`
138
+ }
139
+
140
+ result += `${indent}</${tagName}>`
141
+
142
+ return result
143
+ }
144
+
145
+ // Combine DOM parent chain with VDOM structure into a single HTML-like tree
146
+ function formatCombinedStructure(parent: Element | Node | null, vnode: any, maxParents: number = 4): string {
147
+ if (!parent && !vnode) return ''
148
+
149
+ // Collect DOM parents (from outermost to innermost)
150
+ const domElements: {openTag: string; closeTag: string}[] = []
151
+ let current: Node | null = parent
152
+ let depth = 0
153
+
154
+ while (current && depth < maxParents) {
155
+ if (current.nodeType === 1) {
156
+ // Element node
157
+ const el = current as Element
158
+ // Skip html and body tags - they're not useful context
159
+ if (el.tagName !== 'HTML' && el.tagName !== 'BODY') {
160
+ domElements.unshift(formatDOMElement(el))
161
+ }
162
+ }
163
+ current = current.parentElement || current.parentNode
164
+ depth++
165
+ }
166
+
167
+ // Build the combined output
168
+ const lines: string[] = []
169
+
170
+ // Opening tags for DOM parents
171
+ domElements.forEach((el, i) => {
172
+ lines.push(' '.repeat(i) + el.openTag)
173
+ })
174
+
175
+ // VDOM structure (indented inside the DOM parents)
176
+ if (vnode) {
177
+ const vdomIndent = domElements.length
178
+ const vdomTree = formatVDOMTree(vnode, 4, 0, true)
179
+ if (vdomTree) {
180
+ // Indent each line of the VDOM tree
181
+ const vdomLines = vdomTree.split('\n')
182
+ vdomLines.forEach((line) => {
183
+ lines.push(' '.repeat(vdomIndent) + line)
184
+ })
185
+ }
186
+ }
187
+
188
+ // Closing tags for DOM parents (in reverse order)
189
+ for (let i = domElements.length - 1; i >= 0; i--) {
190
+ lines.push(' '.repeat(i) + domElements[i].closeTag)
191
+ }
192
+
193
+ return lines.join('\n')
194
+ }
195
+
196
+ function buildComponentPath(vnode: any, context?: {oldVnode?: any; newVnode?: any}): string[] {
197
+ const path: string[] = []
198
+
199
+ const traverseVnode = (v: any, depth: number = 0): boolean => {
200
+ if (!v || depth > 10) return false
201
+
202
+ const name = getComponentName(v)
203
+ const isComponent =
204
+ typeof v.tag !== 'string' && name !== 'Unknown' && name !== 'Component' && name !== 'AnonymousComponent'
205
+
206
+ if (isComponent) {
207
+ path.push(name)
208
+ }
209
+
210
+ if (v.instance && depth < 2) {
211
+ if (traverseVnode(v.instance, depth + 1)) return true
212
+ }
213
+
214
+ if (v.children && Array.isArray(v.children) && depth < 2) {
215
+ for (let i = 0; i < Math.min(v.children.length, 3); i++) {
216
+ const child = v.children[i]
217
+ if (child && traverseVnode(child, depth + 1)) return true
218
+ }
219
+ }
220
+
221
+ return false
222
+ }
223
+
224
+ if (context?.newVnode) {
225
+ traverseVnode(context.newVnode)
226
+ if (path.length > 0) return path
227
+ }
228
+ if (context?.oldVnode) {
229
+ traverseVnode(context.oldVnode)
230
+ if (path.length > 0) return path
231
+ }
232
+
233
+ if (vnode) {
234
+ traverseVnode(vnode)
235
+ }
236
+
237
+ return path
238
+ }
239
+
240
+ function formatComponentHierarchy(vnode: any, context?: {oldVnode?: any; newVnode?: any}): string {
241
+ if (!vnode) return 'Unknown'
242
+
243
+ const path = buildComponentPath(vnode, context)
244
+ const immediateName = getComponentName(vnode)
245
+ const isElement = typeof vnode.tag === 'string'
246
+
247
+ if (path.length > 0) {
248
+ const pathStr = path.join(' → ')
249
+ if (isElement && immediateName !== path[path.length - 1]) {
250
+ return `${immediateName} in ${pathStr}`
251
+ } else {
252
+ return pathStr
253
+ }
254
+ }
255
+
256
+ return immediateName
257
+ }
258
+
259
+ export interface HydrationErrorContext {
260
+ parent?: Element
261
+ node?: Node
262
+ matchedNodes?: Set<Node>
263
+ oldVnode?: any
264
+ newVnode?: any
265
+ }
266
+
267
+ export function logHydrationError(
268
+ operation: string,
269
+ vnode: any,
270
+ _element: Element | null,
271
+ error: Error,
272
+ context?: HydrationErrorContext,
273
+ ): void {
274
+ // Update hydration statistics
275
+ updateHydrationStats(vnode)
276
+
277
+ // Throttle error logging to avoid performance issues
278
+ hydrationErrorCount++
279
+ if (hydrationErrorCount > MAX_HYDRATION_ERRORS) {
280
+ if (hydrationErrorCount === MAX_HYDRATION_ERRORS + 1) {
281
+ const topComponents = Array.from(hydrationStats.componentMismatches.entries())
282
+ .toSorted((a: [string, number], b: [string, number]) => b[1] - a[1])
283
+ .slice(0, 5)
284
+ .map(([name, count]: [string, number]) => `${name}: ${count}`)
285
+ .join(', ')
286
+
287
+ logger.warn(
288
+ `hydration errors throttled: more than ${MAX_HYDRATION_ERRORS} errors detected. suppressing further logs to improve performance.`,
289
+ {
290
+ totalMismatches: hydrationStats.totalMismatches,
291
+ topComponents: topComponents || 'none',
292
+ },
293
+ )
294
+ }
295
+ return
296
+ }
297
+
298
+ // Build user-friendly component hierarchy
299
+ const componentHierarchy = formatComponentHierarchy(vnode, context)
300
+
301
+ // Log hydration error with structured context
302
+ const logContext: Record<string, any> = {
303
+ componentPath: componentHierarchy,
304
+ operation,
305
+ }
306
+
307
+ if (context?.node) {
308
+ logContext.affectedNode = context.node.nodeType === 1 ? `${(context.node as Element).tagName.toLowerCase()}` : 'text'
309
+ }
310
+
311
+ // Include structure info in debug mode
312
+ if (HYDRATION_DEBUG) {
313
+ const vnodeToShow = context?.oldVnode || vnode || context?.newVnode
314
+ try {
315
+ const combinedStructure = formatCombinedStructure(context?.parent || null, vnodeToShow, 4)
316
+ if (combinedStructure) {
317
+ logContext.structure = combinedStructure
318
+ }
319
+ } catch (_e) {
320
+ // Fallback: try to show at least the VDOM structure
321
+ if (vnodeToShow) {
322
+ try {
323
+ const vdomTree = formatVDOMTree(vnodeToShow, 4, 0, true)
324
+ if (vdomTree) {
325
+ logContext.vdomStructure = vdomTree
326
+ }
327
+ } catch (_e2) {
328
+ logContext.component = getComponentName(vnodeToShow)
329
+ }
330
+ }
331
+ }
332
+
333
+ // Show what's being removed vs what's replacing it (if both exist)
334
+ if (context?.oldVnode && context?.newVnode) {
335
+ try {
336
+ const oldTree = formatVDOMTree(context.oldVnode, 3)
337
+ const newTree = formatVDOMTree(context.newVnode, 3)
338
+ if (oldTree) logContext.removing = oldTree
339
+ if (newTree) logContext.replacingWith = newTree
340
+ } catch (_e) {
341
+ // Silently fail if formatting doesn't work
342
+ }
343
+ }
344
+ }
345
+
346
+ if (operation.includes('removeChild') || operation.includes('removeDOM')) {
347
+ logContext.handledGracefully = true
348
+ }
349
+
350
+ logger.error(`hydration error: ${operation}`, error, logContext)
351
+ }
352
+
353
+ // Track hydration statistics for debugging
354
+ interface HydrationStats {
355
+ totalMismatches: number
356
+ componentMismatches: Map<string, number>
357
+ lastMismatchTime: number
358
+ }
359
+
360
+ let hydrationStats: HydrationStats = {
361
+ totalMismatches: 0,
362
+ componentMismatches: new Map(),
363
+ lastMismatchTime: 0,
364
+ }
365
+
366
+ export function getHydrationStats(): HydrationStats {
367
+ return {...hydrationStats, componentMismatches: new Map(hydrationStats.componentMismatches)}
368
+ }
369
+
370
+ export function resetHydrationStats(): void {
371
+ hydrationStats = {
372
+ totalMismatches: 0,
373
+ componentMismatches: new Map(),
374
+ lastMismatchTime: 0,
375
+ }
376
+ }
377
+
378
+ // Update stats when hydration error occurs
379
+ function updateHydrationStats(vnode: any): void {
380
+ hydrationStats.totalMismatches++
381
+ hydrationStats.lastMismatchTime = Date.now()
382
+ const componentName = getComponentName(vnode)
383
+ const currentCount = hydrationStats.componentMismatches.get(componentName) || 0
384
+ hydrationStats.componentMismatches.set(componentName, currentCount + 1)
385
+ }
package/util/uri.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Isomorphic URI API - works in both SSR (server) and browser contexts
3
+ */
4
+
5
+ /**
6
+ * Get the current full URL (href)
7
+ * Returns window.location.href in browser, or server's request URL during SSR
8
+ */
9
+ export function getCurrentUrl(): string {
10
+ // Prefer browser location when available (more direct)
11
+ if (typeof window !== 'undefined' && window.location) {
12
+ return window.location.href
13
+ }
14
+
15
+ // Fall back to SSR server URL (when client code runs on server)
16
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__SSR_URL__) {
17
+ return (globalThis as any).__SSR_URL__
18
+ }
19
+
20
+ // Fallback (shouldn't happen)
21
+ return ''
22
+ }
23
+
24
+ /**
25
+ * Parse a URL string into its components
26
+ */
27
+ function parseUrl(url: string): URL {
28
+ try {
29
+ return new URL(url)
30
+ } catch {
31
+ // Fallback for relative URLs
32
+ return new URL(url, 'http://localhost')
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get the current pathname
38
+ */
39
+ export function getPathname(): string {
40
+ // Prefer browser location when available (more direct)
41
+ if (typeof window !== 'undefined' && window.location) {
42
+ return window.location.pathname || '/'
43
+ }
44
+
45
+ // Fall back to parsing SSR URL
46
+ const url = getCurrentUrl()
47
+ if (!url) return '/'
48
+
49
+ const parsed = parseUrl(url)
50
+ return parsed.pathname || '/'
51
+ }
52
+
53
+ /**
54
+ * Get the current search string (query string)
55
+ */
56
+ export function getSearch(): string {
57
+ // Prefer browser location when available (more direct)
58
+ if (typeof window !== 'undefined' && window.location) {
59
+ return window.location.search || ''
60
+ }
61
+
62
+ // Fall back to parsing SSR URL
63
+ const url = getCurrentUrl()
64
+ if (!url) return ''
65
+
66
+ const parsed = parseUrl(url)
67
+ return parsed.search || ''
68
+ }
69
+
70
+ /**
71
+ * Get the current hash
72
+ */
73
+ export function getHash(): string {
74
+ // Prefer browser location when available (more direct)
75
+ if (typeof window !== 'undefined' && window.location) {
76
+ return window.location.hash || ''
77
+ }
78
+
79
+ // Fall back to parsing SSR URL
80
+ const url = getCurrentUrl()
81
+ if (!url) return ''
82
+
83
+ const parsed = parseUrl(url)
84
+ return parsed.hash || ''
85
+ }
86
+
87
+ /**
88
+ * Get a Location-like object with all properties
89
+ * Compatible with browser Location API
90
+ */
91
+ export interface IsomorphicLocation {
92
+ href: string
93
+ pathname: string
94
+ search: string
95
+ hash: string
96
+ origin?: string
97
+ host?: string
98
+ hostname?: string
99
+ port?: string
100
+ protocol?: string
101
+ }
102
+
103
+ export function getLocation(): IsomorphicLocation {
104
+ const url = getCurrentUrl()
105
+ if (!url) {
106
+ return {
107
+ href: '',
108
+ pathname: '/',
109
+ search: '',
110
+ hash: '',
111
+ }
112
+ }
113
+
114
+ const parsed = parseUrl(url)
115
+ return {
116
+ href: parsed.href,
117
+ pathname: parsed.pathname || '/',
118
+ search: parsed.search || '',
119
+ hash: parsed.hash || '',
120
+ origin: parsed.origin,
121
+ host: parsed.host,
122
+ hostname: parsed.hostname,
123
+ port: parsed.port,
124
+ protocol: parsed.protocol,
125
+ }
126
+ }
package/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ import pkg from './package.json' with {type: 'json'}
2
+
3
+ export const version: string = pkg.version