@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,223 @@
1
+ /**
2
+ * Isomorphic logger with colors and structured logging.
3
+ * Works in both server (Bun/Node) and client (browser) environments.
4
+ * Provides info, debug, warning, and error log levels with appropriate formatting.
5
+ */
6
+
7
+ // Detect if we're running in a browser environment
8
+ const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
9
+
10
+ // ANSI color codes for terminal output (server only)
11
+ const colors = {
12
+ reset: '\x1b[0m',
13
+ bright: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+
16
+ // Text colors
17
+ black: '\x1b[30m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ blue: '\x1b[34m',
22
+ magenta: '\x1b[35m',
23
+ cyan: '\x1b[36m',
24
+ white: '\x1b[37m',
25
+
26
+ // Background colors
27
+ bgBlack: '\x1b[40m',
28
+ bgRed: '\x1b[41m',
29
+ bgGreen: '\x1b[42m',
30
+ bgYellow: '\x1b[43m',
31
+ bgBlue: '\x1b[44m',
32
+ bgMagenta: '\x1b[45m',
33
+ bgCyan: '\x1b[46m',
34
+ bgWhite: '\x1b[47m',
35
+ }
36
+
37
+ // ANSI colors for server/terminal. In browser we use %c for proper DevTools styling.
38
+ const enableColors =
39
+ !isBrowser &&
40
+ (typeof process === 'undefined' || !process?.env || (process.env.NO_COLOR !== '1' && process.env.NO_COLOR !== 'true'))
41
+
42
+ function colorize(text: string, color: string): string {
43
+ return enableColors ? `${color}${text}${colors.reset}` : text
44
+ }
45
+
46
+ function getTimestamp(): string {
47
+ const now = new Date()
48
+ const hours = String(now.getHours()).padStart(2, '0')
49
+ const minutes = String(now.getMinutes()).padStart(2, '0')
50
+ const seconds = String(now.getSeconds()).padStart(2, '0')
51
+ const ms = String(now.getMilliseconds()).padStart(3, '0')
52
+ return `${hours}:${minutes}:${seconds}.${ms}`
53
+ }
54
+
55
+ function formatLevel(level: 'info' | 'debug' | 'warn' | 'error'): string {
56
+ const levelMap = {
57
+ info: colorize('info', colors.bright + colors.cyan),
58
+ debug: colorize('debug', colors.bright + colors.blue),
59
+ warn: colorize('warn', colors.bright + colors.yellow),
60
+ error: colorize('error', colors.bright + colors.red),
61
+ }
62
+ return levelMap[level]
63
+ }
64
+
65
+ export interface LogContext {
66
+ pathname?: string
67
+ method?: string
68
+ sessionId?: string
69
+ route?: string
70
+ module?: string // Module name (e.g., 'identity', 'order') - will be shown as [module] prefix
71
+ [key: string]: any
72
+ }
73
+
74
+ class Logger {
75
+ // Default prefix: [ssr] for server infrastructure, [app] for application code
76
+ private prefix: string = '[ssr]'
77
+
78
+ /**
79
+ * Set the log prefix (default: '[ssr]' for infrastructure, '[app]' for application code)
80
+ */
81
+ setPrefix(prefix: string): void {
82
+ this.prefix = prefix
83
+ }
84
+
85
+ private formatMessage(level: 'info' | 'debug' | 'warn' | 'error', message: string, context?: LogContext): string {
86
+ const timestamp = colorize(getTimestamp(), colors.dim + colors.white)
87
+ const levelStr = formatLevel(level)
88
+
89
+ // Always use the set prefix (e.g., [app] or [ssr])
90
+ const prefixStr = colorize(
91
+ this.prefix,
92
+ this.prefix === '[ssr]' ? colors.bright + colors.magenta : colors.bright + colors.cyan,
93
+ )
94
+
95
+ // Include module in message if provided
96
+ let displayMessage = message
97
+ if (context?.module) {
98
+ displayMessage = `[${context.module}] ${message}`
99
+ }
100
+
101
+ let contextStr = ''
102
+ if (context) {
103
+ const contextParts: string[] = []
104
+ if (context.method) {
105
+ contextParts.push(colorize(context.method, colors.cyan))
106
+ }
107
+ if (context.pathname) {
108
+ contextParts.push(colorize(context.pathname, colors.green))
109
+ }
110
+ if (context.route) {
111
+ contextParts.push(colorize(`route:${context.route}`, colors.blue))
112
+ }
113
+ if (context.sessionId) {
114
+ contextParts.push(colorize(`session:${context.sessionId.slice(0, 8)}...`, colors.dim + colors.white))
115
+ }
116
+
117
+ // Add any additional context fields (excluding module which is shown in message)
118
+ for (const [key, value] of Object.entries(context)) {
119
+ if (!['method', 'pathname', 'route', 'sessionId', 'module'].includes(key)) {
120
+ contextParts.push(colorize(`${key}:${value}`, colors.dim + colors.white))
121
+ }
122
+ }
123
+
124
+ if (contextParts.length > 0) {
125
+ contextStr = ' ' + contextParts.join(' ')
126
+ }
127
+ }
128
+
129
+ return `${timestamp} ${prefixStr} ${levelStr}${contextStr} ${displayMessage}`
130
+ }
131
+
132
+ private formatContextForBrowser(context?: LogContext): string[] {
133
+ if (!context) return []
134
+ const parts: string[] = []
135
+ if (context.method) parts.push(`Method: ${context.method}`)
136
+ if (context.pathname) parts.push(`Path: ${context.pathname}`)
137
+ if (context.route) parts.push(`Route: ${context.route}`)
138
+ if (context.sessionId) parts.push(`Session: ${context.sessionId.slice(0, 8)}...`)
139
+ for (const [key, value] of Object.entries(context)) {
140
+ if (!['method', 'pathname', 'route', 'sessionId', 'module'].includes(key)) {
141
+ parts.push(`${key}: ${value}`)
142
+ }
143
+ }
144
+ return parts
145
+ }
146
+
147
+ private logBrowser(
148
+ level: 'info' | 'debug' | 'warn' | 'error',
149
+ message: string,
150
+ context?: LogContext,
151
+ error?: Error | unknown,
152
+ ): void {
153
+ const displayMessage = context?.module ? `[${context.module}] ${message}` : message
154
+ const prefixStyle = this.prefix === '[ssr]' ? 'color: #d946ef; font-weight: bold' : 'color: #3b82f6; font-weight: bold'
155
+ const levelStyles: Record<string, string> = {
156
+ info: 'color: #22d3ee; font-weight: bold',
157
+ debug: 'color: #4ade80; font-weight: bold',
158
+ warn: 'color: #fbbf24; font-weight: bold',
159
+ error: 'color: #ef4444; font-weight: bold',
160
+ }
161
+ const logFn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log
162
+ const contextParts = this.formatContextForBrowser(context)
163
+ if (contextParts.length > 0 || error) {
164
+ console.group(`%c${this.prefix}%c ${level}%c ${displayMessage}`, prefixStyle, levelStyles[level], 'color: inherit')
165
+ contextParts.forEach((part) => logFn(` ${part}`))
166
+ if (error instanceof Error && error.stack) {
167
+ console.error('Stack trace:', error.stack)
168
+ }
169
+ console.groupEnd()
170
+ } else {
171
+ logFn(`%c${this.prefix}%c ${level}%c ${displayMessage}`, prefixStyle, levelStyles[level], 'color: inherit')
172
+ }
173
+ }
174
+
175
+ info(message: string, context?: LogContext): void {
176
+ if (isBrowser) {
177
+ this.logBrowser('info', message, context)
178
+ } else {
179
+ console.log(this.formatMessage('info', message, context))
180
+ }
181
+ }
182
+
183
+ debug(message: string, context?: LogContext): void {
184
+ const shouldLog =
185
+ globalThis.__SSR_MODE__ || (isBrowser && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production')
186
+ if (!shouldLog) return
187
+
188
+ if (isBrowser) {
189
+ this.logBrowser('debug', message, context)
190
+ } else {
191
+ console.log(this.formatMessage('debug', message, context))
192
+ }
193
+ }
194
+
195
+ warn(message: string, context?: LogContext): void {
196
+ if (isBrowser) {
197
+ this.logBrowser('warn', message, context)
198
+ } else {
199
+ console.warn(this.formatMessage('warn', message, context))
200
+ }
201
+ }
202
+
203
+ error(message: string, error?: Error | unknown, context?: LogContext): void {
204
+ const errorMessage = error instanceof Error ? error.message : String(error)
205
+ const baseMessage = error ? `${message}: ${errorMessage}` : message
206
+
207
+ if (isBrowser) {
208
+ this.logBrowser('error', baseMessage, context, error)
209
+ } else {
210
+ console.error(this.formatMessage('error', baseMessage, context))
211
+ if (error instanceof Error && error.stack) {
212
+ const stackTrace = colorize(error.stack, colors.dim + colors.red)
213
+ console.error(stackTrace)
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // Default logger instance for application code (will be set to [app] by app initialization)
220
+ export const logger = new Logger()
221
+
222
+ // Export Logger class for creating custom instances
223
+ export {Logger}
@@ -0,0 +1,87 @@
1
+ // Session management interfaces and default in-memory implementation
2
+
3
+ export interface SessionData {
4
+ userId: string | null
5
+ data: Record<string, any>
6
+ createdAt: Date
7
+ expiresAt: Date
8
+ }
9
+
10
+ export interface SessionStore {
11
+ getSession(sessionId: string): SessionData | null
12
+ updateSession(sessionId: string, data: Record<string, any>): void
13
+ createSession(userId: string | null): string
14
+ }
15
+
16
+ // Default in-memory session store implementation
17
+ // In production, this would be replaced with Redis, database, etc.
18
+ export class MemorySessionStore implements SessionStore {
19
+ private sessions = new Map<string, SessionData>()
20
+
21
+ createSession(userId: string | null): string {
22
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
23
+ const now = Date.now()
24
+ this.sessions.set(sessionId, {
25
+ userId,
26
+ data: {},
27
+ createdAt: new Date(now),
28
+ expiresAt: new Date(now + 1000 * 60 * 60 * 24), // 24 hours
29
+ })
30
+ return sessionId
31
+ }
32
+
33
+ getSession(sessionId: string): SessionData | null {
34
+ const session = this.sessions.get(sessionId)
35
+ if (!session) return null
36
+
37
+ // Check expiration
38
+ if (session.expiresAt.getTime() < Date.now()) {
39
+ this.sessions.delete(sessionId)
40
+ return null
41
+ }
42
+
43
+ return session
44
+ }
45
+
46
+ getSessionByUserId(userId: string): string | null {
47
+ for (const [sessionId, session] of this.sessions.entries()) {
48
+ if (session.userId === userId) {
49
+ // Check expiration
50
+ if (session.expiresAt.getTime() >= Date.now()) {
51
+ return sessionId
52
+ }
53
+ }
54
+ }
55
+ return null
56
+ }
57
+
58
+ updateSession(sessionId: string, data: Record<string, any>): void {
59
+ const session = this.sessions.get(sessionId)
60
+ if (session) {
61
+ // Merge data with existing session data
62
+ // Handle nested structures like session_data
63
+ session.data = {...session.data, ...data}
64
+ }
65
+ }
66
+
67
+ deleteSession(sessionId: string): void {
68
+ this.sessions.delete(sessionId)
69
+ }
70
+
71
+ // Clean up expired sessions
72
+ cleanup(): void {
73
+ const now = Date.now()
74
+ for (const [sessionId, session] of this.sessions.entries()) {
75
+ if (session.expiresAt.getTime() < now) {
76
+ this.sessions.delete(sessionId)
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // Helper function to extract session ID from request cookies
83
+ export function extractSessionId(req: Request): string | null {
84
+ const cookies = req.headers.get('cookie') || ''
85
+ const sessionIdMatch = cookies.match(/sessionId=([^;]+)/)
86
+ return sessionIdMatch ? sessionIdMatch[1] : null
87
+ }
package/server/ssr.ts ADDED
@@ -0,0 +1,205 @@
1
+ import {readFile} from 'fs/promises'
2
+
3
+ import m from '../server'
4
+ import {runWithContextAsync} from '../ssrContext'
5
+
6
+ import {extractSessionId} from './session'
7
+ import {logger} from './ssrLogger'
8
+
9
+ import type {SessionStore} from './session'
10
+ import type {SSRAccessContext} from '../ssrContext'
11
+
12
+ declare global {
13
+ // eslint-disable-next-line @typescript-eslint/naming-convention
14
+ var __SSR_MODE__: boolean | undefined
15
+ // eslint-disable-next-line @typescript-eslint/naming-convention
16
+ var __SSR_URL__: string | undefined
17
+ }
18
+
19
+ globalThis.__SSR_MODE__ = true
20
+
21
+ export interface SSROptions {
22
+ routes: Record<string, any>
23
+ /** Create per-request context (store, stateRegistry, sessionId, sessionData). */
24
+ createRequestContext: (req: Request) => SSRAccessContext
25
+ /** Load store and register state for this request; runs inside request context. */
26
+ initRequestContext: (context: SSRAccessContext) => void | Promise<void>
27
+ getHtmlTemplate: () => Promise<string>
28
+ appSelector?: string // Default: '#app'
29
+ stateScriptId?: string // Default: '__SSR_STATE__'
30
+ }
31
+
32
+ export interface BunSSRConfig {
33
+ port: number
34
+ templatePath: string
35
+ templateRoute?: string // Default: '/__template__'
36
+ htmlTemplate: any // Bun route handler
37
+ }
38
+
39
+ /**
40
+ * Helper function to get Bun's processed HTML template with HMR scripts
41
+ * This ensures HMR scripts and proper asset serving work in development
42
+ */
43
+ export async function getBunProcessedTemplate(
44
+ port: number,
45
+ templatePath: string,
46
+ templateRoute: string = '/__template__',
47
+ ): Promise<string> {
48
+ // Fetch from Bun's route handler to get processed template with HMR scripts
49
+ // Use a special route that Bun processes but we don't use for SSR
50
+ const templateUrl = `http://localhost:${port}${templateRoute}`
51
+ try {
52
+ const response = await fetch(templateUrl)
53
+ if (response.ok) {
54
+ return await response.text()
55
+ }
56
+ } catch {
57
+ // Fallback: read template file directly (won't have HMR, but will work)
58
+ }
59
+
60
+ return await readFile(templatePath, 'utf-8')
61
+ }
62
+
63
+ /**
64
+ * Helper to check if a pathname is a Bun internal asset (should be handled by Bun)
65
+ */
66
+ export function shouldHandleBunAssets(pathname: string): boolean {
67
+ return pathname.startsWith('/_bun/')
68
+ }
69
+
70
+ /**
71
+ * Create Bun.serve() configuration with template route for HMR support
72
+ */
73
+ export function createBunSSRConfig(config: BunSSRConfig) {
74
+ return {
75
+ port: config.port,
76
+ routes: {
77
+ // Let Bun process the HTML template for HMR and asset serving
78
+ // Use a special route that we don't use for SSR, so fetch handler can intercept SSR routes
79
+ [config.templateRoute || '/__template__']: config.htmlTemplate,
80
+ },
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Create SSR response with serialized state and session cookie.
86
+ * Runs the whole request inside runWithContextAsync so getSSRContext() returns
87
+ * this request's context; initRequestContext then loads the store and registers state.
88
+ */
89
+ export async function createSSRResponse(pathname: string, req: Request, options: SSROptions): Promise<Response> {
90
+ const context = options.createRequestContext(req)
91
+
92
+ return runWithContextAsync(context, async () => {
93
+ try {
94
+ await options.initRequestContext(context)
95
+
96
+ globalThis.__SSR_URL__ = req.url
97
+
98
+ const routeCount = Object.keys(options.routes).length
99
+ logger.debug('resolving route', {pathname, routeCount, routeExists: !!options.routes[pathname]})
100
+
101
+ const result = await m.route.resolve(pathname, options.routes, m.renderToString)
102
+
103
+ const appHtml = typeof result === 'string' ? result : result.html
104
+ const serializedState = typeof result === 'string' ? {} : result.state
105
+ const htmlLength = appHtml?.length || 0
106
+
107
+ logger.debug('route resolved', {pathname, htmlLength, resultType: typeof result === 'string' ? 'string' : 'object'})
108
+
109
+ if (!appHtml || appHtml.trim() === '' || appHtml.trim() === '<div></div>') {
110
+ logger.warn('empty html rendered', {pathname})
111
+ } else {
112
+ logger.info(`get ${pathname} → ${htmlLength} chars`)
113
+ }
114
+
115
+ let html = await options.getHtmlTemplate()
116
+
117
+ const appSelector = options.appSelector || '#app'
118
+
119
+ let openingTagPattern: string
120
+ if (appSelector.startsWith('#')) {
121
+ const id = appSelector.slice(1)
122
+ openingTagPattern = `<([a-zA-Z][a-zA-Z0-9]*)\\s+id="${id}"([^>]*)>`
123
+ } else if (appSelector.startsWith('.')) {
124
+ const className = appSelector.slice(1)
125
+ openingTagPattern = `<([a-zA-Z][a-zA-Z0-9]*)([^>]*\\s+class="[^"]*\\b${className}\\b[^"]*"[^>]*)>`
126
+ } else {
127
+ openingTagPattern = `<${appSelector}([^>]*)>`
128
+ }
129
+
130
+ const fullPattern = new RegExp(`(${openingTagPattern})\\s*</([a-zA-Z][a-zA-Z0-9]*)>`, 'i')
131
+ html = html.replace(fullPattern, (_match, openingTag, closingTagName) => {
132
+ const tagMatch = openingTag.match(/^<([a-zA-Z][a-zA-Z0-9]*)/)
133
+ const elementName = tagMatch ? tagMatch[1] : closingTagName || 'div'
134
+ return `${openingTag}${appHtml}</${elementName}>`
135
+ })
136
+
137
+ const stateScriptId = options.stateScriptId || '__SSR_STATE__'
138
+ const stateScript = `<script id="${stateScriptId}" type="application/json">${JSON.stringify(serializedState)}</script>`
139
+ html = html.replace('</head>', `${stateScript}</head>`)
140
+
141
+ const sessionId = context.sessionId ?? ''
142
+
143
+ return new Response(html, {
144
+ headers: {
145
+ // eslint-disable-next-line @typescript-eslint/naming-convention
146
+ 'Content-Type': 'text/html; charset=utf-8',
147
+ // eslint-disable-next-line @typescript-eslint/naming-convention
148
+ 'Set-Cookie': `sessionId=${sessionId}; Path=/; HttpOnly; SameSite=Lax`,
149
+ },
150
+ })
151
+ } catch (error) {
152
+ logger.error('ssr request failed', error, {
153
+ pathname,
154
+ method: req.method,
155
+ sessionId: context.sessionId,
156
+ })
157
+ return new Response('Internal Server Error', {status: 500})
158
+ }
159
+ })
160
+ }
161
+
162
+ /**
163
+ * Create session update API handler factory
164
+ * Returns a handler function for POST /api/session requests
165
+ */
166
+ export function createSessionUpdateHandler(
167
+ sessionStore: SessionStore,
168
+ extractSessionIdFn: (req: Request) => string | null = extractSessionId,
169
+ ): (req: Request) => Promise<Response> {
170
+ return async (req: Request): Promise<Response> => {
171
+ const sessionId = extractSessionIdFn(req)
172
+
173
+ if (!sessionId) {
174
+ return new Response('No session ID', {status: 401})
175
+ }
176
+
177
+ try {
178
+ const body = await req.json()
179
+ // Blueprint extracts { session: {...} }, so body.session contains the actual session data
180
+ // body structure: { session: { user: {...}, serverData: '...', lastServerUpdate: ... } }
181
+ const sessionData = body.session_data || body.session || {} // Support both session_data and session for compatibility
182
+
183
+ // Update session store with new session state
184
+ // Store under session_data key in sessionStore.data
185
+ // sessionData structure: { user: {...}, serverData: '...', lastServerUpdate: ... }
186
+ sessionStore.updateSession(sessionId, {session_data: sessionData})
187
+
188
+ logger.debug('session updated', {
189
+ method: req.method,
190
+ sessionId,
191
+ })
192
+
193
+ return new Response(JSON.stringify({success: true}), {
194
+ // eslint-disable-next-line @typescript-eslint/naming-convention
195
+ headers: {'Content-Type': 'application/json'},
196
+ })
197
+ } catch (error) {
198
+ logger.error('failed to update session', error, {
199
+ method: req.method,
200
+ sessionId,
201
+ })
202
+ return new Response('Internal Server Error', {status: 500})
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SSR-specific logger instance.
3
+ * SSR infrastructure code should import {logger} from this file
4
+ * to get a logger instance with [ssr] prefix.
5
+ */
6
+
7
+ import {Logger} from './logger'
8
+
9
+ // Create SSR logger instance with [ssr] prefix
10
+ const logger = new Logger()
11
+ logger.setPrefix('[ssr]')
12
+
13
+ export {logger}
package/server.ts ADDED
@@ -0,0 +1,101 @@
1
+ import hyperscript from './render/hyperscript'
2
+ import {logger} from './server/logger'
3
+ import {version} from './version'
4
+ import {renderToStringFactory} from './render/renderToString'
5
+
6
+ import routerFactory from './api/router'
7
+ import parseQueryString from './querystring/parse'
8
+ import buildQueryString from './querystring/build'
9
+ import parsePathname from './pathname/parse'
10
+ import buildPathname from './pathname/build'
11
+ import Vnode from './render/vnode'
12
+ import censor from './util/censor'
13
+
14
+ import type {MithrilStatic, Hyperscript} from './index'
15
+ import type {Redraw} from './api/mount-redraw'
16
+
17
+ logger.debug(`mithril ssr v${version}`)
18
+
19
+ // Create server-side renderer
20
+ const {renderToString, renderToStringSync} = renderToStringFactory()
21
+
22
+ // Create isomorphic router instance (null window for server, mock mountRedraw)
23
+ const router = routerFactory(null, {
24
+ mount: () => {
25
+ throw new Error('m.mount is not available on server. Use m.route.resolve instead.')
26
+ },
27
+ redraw: () => {
28
+ throw new Error('m.redraw is not available on server.')
29
+ },
30
+ })
31
+
32
+ // Set prefix to empty string for pathname-based routing (not hash-based)
33
+ router.prefix = ''
34
+
35
+ // Expose resolve method and Link component for server-side routing
36
+ const routerServer = {
37
+ resolve: router.resolve.bind(router),
38
+ Link: router.Link,
39
+ prefix: router.prefix,
40
+ get: router.get.bind(router),
41
+ set: router.set.bind(router),
42
+ param: router.param.bind(router),
43
+ params: router.params,
44
+ link: router.link.bind(router),
45
+ redirect: router.redirect.bind(router),
46
+ REDIRECT: router.REDIRECT,
47
+ SKIP: router.SKIP,
48
+ }
49
+
50
+ // Server-side Mithril instance
51
+ const mServer: MithrilStatic &
52
+ Hyperscript & {
53
+ renderToString: typeof renderToString
54
+ renderToStringSync: typeof renderToStringSync
55
+ route: typeof routerServer
56
+ } = function m(this: any) {
57
+ return hyperscript.apply(this, arguments as any)
58
+ } as unknown as MithrilStatic &
59
+ Hyperscript & {
60
+ renderToString: typeof renderToString
61
+ renderToStringSync: typeof renderToStringSync
62
+ route: typeof routerServer
63
+ }
64
+
65
+ mServer.m = hyperscript as Hyperscript
66
+ mServer.trust = hyperscript.trust
67
+ mServer.fragment = hyperscript.fragment
68
+ mServer.Fragment = '['
69
+ mServer.renderToString = renderToString
70
+ mServer.renderToStringSync = renderToStringSync
71
+ mServer.route = routerServer as any
72
+ mServer.parseQueryString = parseQueryString
73
+ mServer.buildQueryString = buildQueryString
74
+ mServer.parsePathname = parsePathname
75
+ mServer.buildPathname = buildPathname
76
+ mServer.vnode = Vnode
77
+ mServer.censor = censor
78
+
79
+ // Placeholder implementations for server (not used but needed for type compatibility)
80
+ mServer.mount = () => {
81
+ throw new Error('m.mount is not available on server. Use m.renderToString instead.')
82
+ }
83
+ mServer.render = () => {
84
+ throw new Error('m.render is not available on server. Use m.renderToString instead.')
85
+ }
86
+ mServer.redraw = Object.assign(
87
+ () => {
88
+ throw new Error('m.redraw is not available on server.')
89
+ },
90
+ {
91
+ sync: () => {
92
+ throw new Error('m.redraw.sync is not available on server.')
93
+ },
94
+ },
95
+ ) as Redraw
96
+
97
+ export default mServer
98
+
99
+ // Export SSR server utilities
100
+ export * from './server/session'
101
+ export * from './server/ssr'