@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,494 @@
1
+ import {setCurrentComponent, clearCurrentComponent} from '../signal'
2
+
3
+ import {serializeAllStates} from './ssrState'
4
+ import Vnode from './vnode'
5
+
6
+ import type {Vnode as VnodeType, Children} from '../index'
7
+
8
+ // Void elements that don't have closing tags
9
+ const VOID_ELEMENTS = new Set([
10
+ 'area',
11
+ 'base',
12
+ 'br',
13
+ 'col',
14
+ 'embed',
15
+ 'hr',
16
+ 'img',
17
+ 'input',
18
+ 'link',
19
+ 'meta',
20
+ 'param',
21
+ 'source',
22
+ 'track',
23
+ 'wbr',
24
+ ])
25
+
26
+ export interface RenderToStringOptions {
27
+ escapeAttribute?: (value: any) => string
28
+ escapeText?: (value: any) => string
29
+ strict?: boolean // Close all empty tags
30
+ xml?: boolean // XML mode (implies strict)
31
+ }
32
+
33
+ // Default escape functions
34
+ function escapeAttributeDefault(value: any): string {
35
+ const str = String(value)
36
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
37
+ }
38
+
39
+ function escapeTextDefault(value: any): string {
40
+ const str = String(value)
41
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
42
+ }
43
+
44
+ function isVoidElement(tag: string): boolean {
45
+ return VOID_ELEMENTS.has(tag.toLowerCase())
46
+ }
47
+
48
+ // Promise tracker for async data fetching
49
+ class PromiseTracker {
50
+ private promises: Promise<any>[] = []
51
+
52
+ waitFor(promise: Promise<any>) {
53
+ this.promises.push(promise)
54
+ }
55
+
56
+ async waitAll(): Promise<void> {
57
+ if (this.promises.length > 0) {
58
+ await Promise.all(this.promises)
59
+ this.promises = []
60
+ }
61
+ }
62
+
63
+ hasPromises(): boolean {
64
+ return this.promises.length > 0
65
+ }
66
+
67
+ reset() {
68
+ this.promises = []
69
+ }
70
+ }
71
+
72
+ // Serialize attributes to HTML string
73
+ function serializeAttributes(attrs: Record<string, any> | null | undefined, options: Required<RenderToStringOptions>): string {
74
+ if (!attrs) return ''
75
+
76
+ const parts: string[] = []
77
+
78
+ for (const key in attrs) {
79
+ const value = attrs[key]
80
+
81
+ // Skip lifecycle hooks and special attributes
82
+ if (
83
+ key === 'key' ||
84
+ key === 'oninit' ||
85
+ key === 'oncreate' ||
86
+ key === 'onupdate' ||
87
+ key === 'onremove' ||
88
+ key === 'onbeforeremove' ||
89
+ key === 'onbeforeupdate' ||
90
+ (key.startsWith('on') && typeof value === 'function')
91
+ ) {
92
+ continue
93
+ }
94
+
95
+ if (value == null) continue
96
+
97
+ // Handle className -> class
98
+ const attrName = key === 'className' ? 'class' : key
99
+
100
+ if (typeof value === 'boolean') {
101
+ if (value) {
102
+ parts.push(attrName)
103
+ }
104
+ } else if (typeof value === 'object') {
105
+ // Handle style objects
106
+ if (key === 'style') {
107
+ const styleStr = Object.entries(value)
108
+ .map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${v}`)
109
+ .join('; ')
110
+ parts.push(`${attrName}="${options.escapeAttribute(styleStr)}"`)
111
+ } else {
112
+ // For other objects, stringify
113
+ parts.push(`${attrName}="${options.escapeAttribute(JSON.stringify(value))}"`)
114
+ }
115
+ } else {
116
+ parts.push(`${attrName}="${options.escapeAttribute(value)}"`)
117
+ }
118
+ }
119
+
120
+ return parts.length > 0 ? ' ' + parts.join(' ') : ''
121
+ }
122
+
123
+ // Serialize text node
124
+ function serializeText(text: string | number, options: Required<RenderToStringOptions>): string {
125
+ return options.escapeText(text)
126
+ }
127
+
128
+ // Serialize component (sync version - no async handling)
129
+ function serializeComponentSync(
130
+ vnode: VnodeType,
131
+ options: Required<RenderToStringOptions>,
132
+ promiseTracker: PromiseTracker,
133
+ ): string {
134
+ const component = vnode.tag as any
135
+
136
+ // Initialize component state
137
+ let state: any
138
+ let view: ((vnode: any) => any) | undefined
139
+
140
+ if (typeof component.view === 'function') {
141
+ // Component object
142
+ state = Object.create(component)
143
+ view = state.view
144
+ } else {
145
+ // Component factory/class
146
+ if (component.prototype && typeof component.prototype.view === 'function') {
147
+ state = new component(vnode)
148
+ } else {
149
+ state = component(vnode)
150
+ }
151
+ view = state.view
152
+ }
153
+
154
+ if (!view) {
155
+ return ''
156
+ }
157
+
158
+ vnode.state = state
159
+
160
+ // Call oninit with context (sync mode - don't await)
161
+ if (typeof state.oninit === 'function') {
162
+ try {
163
+ const context = {
164
+ isSSR: true,
165
+ isHydrating: false,
166
+ }
167
+ // Call oninit but don't wait for it (sync mode)
168
+ state.oninit(vnode, context)
169
+ } catch (_e) {
170
+ // Ignore errors
171
+ }
172
+ }
173
+
174
+ // Call view (bind this to state)
175
+ const instance = Vnode.normalize(view.call(state, vnode))
176
+ if (instance === vnode) {
177
+ throw Error('A view cannot return the vnode it received as argument')
178
+ }
179
+
180
+ vnode.instance = instance
181
+
182
+ // Serialize the instance
183
+ if (instance != null) {
184
+ return serializeNodeSync(instance, options, promiseTracker)
185
+ }
186
+
187
+ return ''
188
+ }
189
+
190
+ // Serialize a single vnode to HTML string (sync version)
191
+ function serializeNodeSync(
192
+ vnode: VnodeType | null,
193
+ options: Required<RenderToStringOptions>,
194
+ promiseTracker: PromiseTracker,
195
+ ): string {
196
+ if (vnode == null) return ''
197
+
198
+ const tag = vnode.tag
199
+
200
+ // Text node
201
+ if (tag === '#') {
202
+ return serializeText(vnode.children as string | number, options)
203
+ }
204
+
205
+ // HTML/trust node
206
+ if (tag === '<') {
207
+ return vnode.children as string
208
+ }
209
+
210
+ // Fragment
211
+ if (tag === '[') {
212
+ const children = vnode.children as (VnodeType | null)[]
213
+ if (!children) return ''
214
+ return children.map((child) => serializeNodeSync(child, options, promiseTracker)).join('')
215
+ }
216
+
217
+ // Component
218
+ if (typeof tag !== 'string') {
219
+ return serializeComponentSync(vnode, options, promiseTracker)
220
+ }
221
+
222
+ // Element
223
+ const attrs = serializeAttributes(vnode.attrs, options)
224
+ const children = vnode.children as Children
225
+
226
+ let html = `<${tag}${attrs}`
227
+
228
+ const isVoid = isVoidElement(tag)
229
+ const shouldSelfClose = (options.strict || options.xml) && isVoid
230
+
231
+ if (shouldSelfClose) {
232
+ html += options.xml ? ' />' : '>'
233
+ return html
234
+ }
235
+
236
+ html += '>'
237
+
238
+ // Serialize children
239
+ if (children != null) {
240
+ if (Array.isArray(children)) {
241
+ html += children.map((child) => serializeNodeSync(child as VnodeType | null, options, promiseTracker)).join('')
242
+ } else if (typeof children === 'string' || typeof children === 'number') {
243
+ html += serializeText(children, options)
244
+ } else if (children != null) {
245
+ html += serializeNodeSync(children as unknown as VnodeType, options, promiseTracker)
246
+ }
247
+ }
248
+
249
+ // Always close non-void elements
250
+ if (!isVoid) {
251
+ html += `</${tag}>`
252
+ }
253
+
254
+ return html
255
+ }
256
+
257
+ // Serialize a single vnode to HTML string
258
+ async function serializeNode(
259
+ vnode: VnodeType | null,
260
+ options: Required<RenderToStringOptions>,
261
+ promiseTracker: PromiseTracker,
262
+ isServer: boolean,
263
+ ): Promise<string> {
264
+ if (vnode == null) return ''
265
+
266
+ const tag = vnode.tag
267
+
268
+ // Text node
269
+ if (tag === '#') {
270
+ return serializeText(vnode.children as string | number, options)
271
+ }
272
+
273
+ // HTML/trust node
274
+ if (tag === '<') {
275
+ return vnode.children as string
276
+ }
277
+
278
+ // Fragment
279
+ if (tag === '[') {
280
+ const children = vnode.children as (VnodeType | null)[]
281
+ if (!children) return ''
282
+ const results = await Promise.all(children.map((child) => serializeNode(child, options, promiseTracker, isServer)))
283
+ return results.join('')
284
+ }
285
+
286
+ // Component
287
+ if (typeof tag !== 'string') {
288
+ return await serializeComponent(vnode, options, promiseTracker, isServer)
289
+ }
290
+
291
+ // Element
292
+ const attrs = serializeAttributes(vnode.attrs, options)
293
+ const children = vnode.children as Children
294
+
295
+ let html = `<${tag}${attrs}`
296
+
297
+ const isVoid = isVoidElement(tag)
298
+ const shouldSelfClose = (options.strict || options.xml) && isVoid
299
+
300
+ if (shouldSelfClose) {
301
+ html += options.xml ? ' />' : '>'
302
+ return html
303
+ }
304
+
305
+ html += '>'
306
+
307
+ // Serialize children
308
+ if (children != null) {
309
+ if (Array.isArray(children)) {
310
+ const results = await Promise.all(
311
+ children.map((child) => serializeNode(child as VnodeType | null, options, promiseTracker, isServer)),
312
+ )
313
+ html += results.join('')
314
+ } else if (typeof children === 'string' || typeof children === 'number') {
315
+ html += serializeText(children, options)
316
+ } else if (children != null) {
317
+ html += await serializeNode(children as unknown as VnodeType, options, promiseTracker, isServer)
318
+ }
319
+ }
320
+
321
+ // Always close non-void elements
322
+ if (!isVoid) {
323
+ html += `</${tag}>`
324
+ }
325
+
326
+ return html
327
+ }
328
+
329
+ // Serialize component
330
+ async function serializeComponent(
331
+ vnode: VnodeType,
332
+ options: Required<RenderToStringOptions>,
333
+ promiseTracker: PromiseTracker,
334
+ isServer: boolean,
335
+ ): Promise<string> {
336
+ const component = vnode.tag as any
337
+
338
+ // Initialize component state
339
+ let state: any
340
+ let view: ((vnode: any) => any) | undefined
341
+
342
+ if (typeof component.view === 'function') {
343
+ // Component object
344
+ state = Object.create(component)
345
+ view = state.view
346
+ } else {
347
+ // Component factory/class
348
+ if (component.prototype && typeof component.prototype.view === 'function') {
349
+ state = new component(vnode)
350
+ } else {
351
+ state = component(vnode)
352
+ }
353
+ view = state.view
354
+ }
355
+
356
+ if (!view) {
357
+ return ''
358
+ }
359
+
360
+ vnode.state = state
361
+
362
+ // Set current component for signal tracking (needed for store access during SSR)
363
+ // This ensures store properties can track component dependencies correctly
364
+ setCurrentComponent(state)
365
+
366
+ let instance: any
367
+ try {
368
+ // Call oninit with context if on server
369
+ if (isServer && typeof state.oninit === 'function') {
370
+ const context = {
371
+ isSSR: true,
372
+ isHydrating: false,
373
+ }
374
+ try {
375
+ const result = state.oninit(vnode, context)
376
+ // If oninit returns a promise, await it
377
+ if (result && typeof result.then === 'function') {
378
+ await result
379
+ }
380
+ } catch (_e) {
381
+ // Ignore errors in oninit for now
382
+ }
383
+ }
384
+
385
+ // Call view (bind this to state)
386
+ instance = Vnode.normalize(view.call(state, vnode))
387
+ if (instance === vnode) {
388
+ throw Error('A view cannot return the vnode it received as argument')
389
+ }
390
+ } finally {
391
+ // Clear current component after rendering
392
+ clearCurrentComponent()
393
+ }
394
+
395
+ vnode.instance = instance
396
+
397
+ // Serialize the instance
398
+ if (instance != null) {
399
+ return serializeNode(instance, options, promiseTracker, isServer)
400
+ }
401
+
402
+ return ''
403
+ }
404
+
405
+ export function renderToStringFactory() {
406
+ const defaultOptions: Required<RenderToStringOptions> = {
407
+ escapeAttribute: escapeAttributeDefault,
408
+ escapeText: escapeTextDefault,
409
+ strict: false,
410
+ xml: false,
411
+ }
412
+
413
+ // Async version (waits for promises)
414
+ async function renderToString(
415
+ vnodes: Children | VnodeType | null,
416
+ options?: RenderToStringOptions,
417
+ ): Promise<{html: string; state: Record<string, any>}> {
418
+ const opts: Required<RenderToStringOptions> = {
419
+ ...defaultOptions,
420
+ ...options,
421
+ escapeAttribute: options?.escapeAttribute || defaultOptions.escapeAttribute,
422
+ escapeText: options?.escapeText || defaultOptions.escapeText,
423
+ }
424
+
425
+ // Normalize vnodes
426
+ const normalized = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
427
+
428
+ const promiseTracker = new PromiseTracker()
429
+
430
+ // First pass: render and collect promises
431
+ let html = ''
432
+ const htmlParts: Promise<string>[] = []
433
+ for (const vnode of normalized) {
434
+ if (vnode != null) {
435
+ htmlParts.push(serializeNode(vnode, opts, promiseTracker, true))
436
+ }
437
+ }
438
+ html = (await Promise.all(htmlParts)).join('')
439
+
440
+ // Wait for all promises
441
+ if (promiseTracker.hasPromises()) {
442
+ await promiseTracker.waitAll()
443
+
444
+ // Second pass: re-render after promises resolve
445
+ promiseTracker.reset()
446
+ html = ''
447
+ const htmlParts2: Promise<string>[] = []
448
+ for (const vnode of normalized) {
449
+ if (vnode != null) {
450
+ htmlParts2.push(serializeNode(vnode, opts, promiseTracker, true))
451
+ }
452
+ }
453
+ html = (await Promise.all(htmlParts2)).join('')
454
+ }
455
+
456
+ // Serialize all registered stores
457
+ const state = serializeAllStates()
458
+
459
+ return {html, state}
460
+ }
461
+
462
+ // Sync version (no promise waiting)
463
+ function renderToStringSync(vnodes: Children | VnodeType | null, options?: RenderToStringOptions): string {
464
+ const opts: Required<RenderToStringOptions> = {
465
+ ...defaultOptions,
466
+ ...options,
467
+ escapeAttribute: options?.escapeAttribute || defaultOptions.escapeAttribute,
468
+ escapeText: options?.escapeText || defaultOptions.escapeText,
469
+ }
470
+
471
+ const normalized = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
472
+
473
+ const promiseTracker = new PromiseTracker()
474
+
475
+ // For sync version, we need to handle async functions synchronously
476
+ // This means we won't wait for promises, but we'll still render what we can
477
+ let html = ''
478
+ for (const vnode of normalized) {
479
+ if (vnode != null) {
480
+ // In sync mode, we can't await, so we'll render synchronously
481
+ // Components with async oninit won't have their data, but that's expected
482
+ const result = serializeNodeSync(vnode, opts, promiseTracker)
483
+ html += result
484
+ }
485
+ }
486
+
487
+ return html
488
+ }
489
+
490
+ return {
491
+ renderToString,
492
+ renderToStringSync,
493
+ }
494
+ }