@bitstillery/mithril 3.9.0 → 3.9.1

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/api/router.ts CHANGED
@@ -26,6 +26,15 @@ export interface RouteResolver<Attrs = Record<string, any>, State = any> {
26
26
  export type SSRState = Record<string, any>
27
27
  export type SSRResult = string | {html: string; state: SSRState}
28
28
 
29
+ /**
30
+ * A route parameter as the URL can carry it. Path params are always strings; the querystring parser
31
+ * also turns `true`/`false` into booleans and `a[]=`/`a[b]=` keys into arrays and objects. A plain
32
+ * object in `history.state` is merged over the params too, so values put there must keep to this
33
+ * shape for the type to hold.
34
+ */
35
+ export type RouteParamValue = string | boolean | RouteParamValue[] | {[key: string]: RouteParamValue}
36
+ export type RouteParams = Record<string, RouteParamValue>
37
+
29
38
  export interface Route {
30
39
  (path: string, params?: Record<string, any>, shouldReplaceHistory?: boolean): void
31
40
  (path: string, component: ComponentType, shouldReplaceHistory?: boolean): void
@@ -33,7 +42,10 @@ export interface Route {
33
42
  get: () => string
34
43
  prefix: string
35
44
  link: (vnode: VnodeType) => string
36
- param: (key?: string) => any
45
+ param: {
46
+ (key: string): RouteParamValue | undefined
47
+ (): RouteParams
48
+ }
37
49
  params: Record<string, any>
38
50
  Link: ComponentType
39
51
  SKIP: {}
package/index.ts CHANGED
@@ -160,7 +160,7 @@ export type {
160
160
  } from './render/vnode'
161
161
  export {MithrilComponent}
162
162
  export type {Hyperscript} from './render/hyperscript'
163
- export type {Route, RouteResolver, RedirectObject} from './api/router'
163
+ export type {Route, RouteParams, RouteParamValue, RouteResolver, RedirectObject} from './api/router'
164
164
  export type {Render, Redraw, Mount} from './api/mount-redraw'
165
165
 
166
166
  // Namespace merge: enables m.Vnode<Attrs> and m.Children when using import m from '@bitstillery/mithril'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitstillery/mithril",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
4
  "description": "Mithril + Signals, Store and SSR",
5
5
  "license": "MIT",
6
6
  "author": "Bitstillery",
@@ -60,7 +60,7 @@
60
60
  "bench:signal": "NODE_ENV=production bun run bench/index.ts signal",
61
61
  "bench:state": "NODE_ENV=production bun run bench/index.ts state",
62
62
  "bench:profile": "bun --cpu-prof-md run bench/index.ts",
63
- "prepublishOnly": "bun run lint:ts",
63
+ "prepublishOnly": "oxfmt --check && bun run lint:ts-syntax && bun run lint:ts-types",
64
64
  "pack:dry-run": "npm pack --dry-run",
65
65
  "release": "./scripts/release.sh",
66
66
  "release:patch": "./scripts/release.sh --release-as patch",
package/render/vnode.ts CHANGED
@@ -28,19 +28,26 @@ export type VnodeDOM<Attrs = Record<string, any>, State = any> = ComponentVnode<
28
28
  */
29
29
  export type ComponentVnode<Attrs = Record<string, any>, State = any> = Omit<Vnode<Attrs, State>, 'attrs'> & {attrs: Attrs}
30
30
 
31
+ /**
32
+ * The hooks are declared as methods, not function-typed properties: under `strictFunctionTypes` a
33
+ * property's parameters are checked contravariantly, so `Component<{name: string}>` would not be a
34
+ * `Component<Record<string, any>>` and `m(Icon, attrs)` could not be passed where `Children` is expected.
35
+ * Method parameters are bivariant, which matches how Mithril really calls them — with the vnode it
36
+ * built for that component.
37
+ */
31
38
  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
+ oninit?(vnode: ComponentVnode<Attrs, State>): void
40
+ oncreate?(vnode: ComponentVnode<Attrs, State>): void
41
+ onbeforeupdate?(vnode: ComponentVnode<Attrs, State>, old: ComponentVnode<Attrs, State>): boolean | void
42
+ onupdate?(vnode: ComponentVnode<Attrs, State>): void
43
+ onbeforeremove?(vnode: ComponentVnode<Attrs, State>): Promise<any> | void
44
+ onremove?(vnode: ComponentVnode<Attrs, State>): void
45
+ view(vnode: ComponentVnode<Attrs, State>): Children | Vnode | null
39
46
  }
40
47
 
41
48
  export interface ComponentFactory<Attrs = Record<string, any>, State = any> {
42
49
  (...args: any[]): Component<Attrs, State>
43
- view?: (vnode: ComponentVnode<Attrs, State>) => Children | Vnode | null
50
+ view?(vnode: ComponentVnode<Attrs, State>): Children | Vnode | null
44
51
  }
45
52
 
46
53
  export type ComponentType<Attrs = Record<string, any>, State = any> =
package/state.ts CHANGED
@@ -232,15 +232,17 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
232
232
 
233
233
  // Handle arrays
234
234
  if (Array.isArray(obj)) {
235
- // Arrays don't get their own signalMap - they use the parent's
236
- // Nested objects AND arrays should be recursively wrapped
237
- const signals = obj.map((item: any) => {
235
+ // Init, push/unshift, splice and index assignment all wrap through here, so an element typed as
236
+ // `State<E>` is one: objects and arrays become their own state proxy (the proxy is the element, not a
237
+ // signal around it), everything else a signal.
238
+ const toElement = (item: any) => {
238
239
  if (typeof item === 'object' && item !== null) {
239
- // Recursively wrap nested objects AND arrays in Proxies
240
240
  return initializeSignals(item, undefined, context)
241
241
  }
242
242
  return toSignal(item)
243
- })
243
+ }
244
+ // Arrays don't get their own signalMap - they use the parent's
245
+ const signals = obj.map(toElement)
244
246
 
245
247
  // List of mutating array methods that should trigger the parent signal
246
248
  const mutatingMethods = new Set([
@@ -354,14 +356,7 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
354
356
  const deleteCount = args[1] ?? signals.length - start
355
357
  const newItems = args.slice(2)
356
358
 
357
- // Convert new items - nested arrays/objects become Proxies, primitives become Signals
358
- const newSignals = newItems.map((item: any) => {
359
- if (typeof item === 'object' && item !== null) {
360
- // Wrap objects/arrays in Proxies (NOT in Signals - the Proxy IS the value)
361
- return initializeSignals(item, undefined, context)
362
- }
363
- return toSignal(item)
364
- })
359
+ const newSignals = newItems.map(toElement)
365
360
 
366
361
  // Update the signals array (target is signals array)
367
362
  const removed = signals.splice(start, deleteCount, ...newSignals)
@@ -399,14 +394,7 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
399
394
  let result
400
395
  if (propStr === 'push' || propStr === 'unshift') {
401
396
  const newItems = args
402
- // Convert new items - nested arrays/objects become Proxies, primitives become Signals
403
- const newSignals = newItems.map((item: any) => {
404
- if (typeof item === 'object' && item !== null) {
405
- // Wrap objects/arrays in Proxies (NOT in Signals - the Proxy IS the value)
406
- return initializeSignals(item, undefined, context)
407
- }
408
- return toSignal(item)
409
- })
397
+ const newSignals = newItems.map(toElement)
410
398
  if (propStr === 'push') {
411
399
  result = signals.push(...newSignals)
412
400
  } else {
@@ -449,9 +437,11 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
449
437
  const fillValue = args[0]
450
438
  const start = args[1] ?? 0
451
439
  const end = args[2] ?? signals.length
452
- const fillSignal = toSignal(fillValue)
440
+ // A wrap per slot, so no two slots share a signal. An object fill value still reads
441
+ // back as one shared element in every slot, as native fill shares the reference:
442
+ // `toElement` returns the cached proxy for the same object.
453
443
  for (let i = start; i < end; i++) {
454
- signals[i] = fillSignal
444
+ signals[i] = toElement(fillValue)
455
445
  }
456
446
  result = signals.length
457
447
  } else {
@@ -493,12 +483,14 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
493
483
  set(target, prop, value) {
494
484
  if (typeof prop === 'string' && !isNaN(Number(prop))) {
495
485
  const index = Number(prop)
496
- if (index >= 0 && index < signals.length) {
486
+ // Assigning past the end (`arr[arr.length] = x`) appends, so it wraps and notifies like push.
487
+ if ((index >= 0 && index < signals.length) || (Number.isInteger(index) && index >= signals.length)) {
497
488
  const sig = signals[index]
498
- if (isSignal(sig)) {
489
+ // A primitive over a primitive keeps the element's signal, so `arr.$i` subscribers see it.
490
+ if (isSignal(sig) && (typeof value !== 'object' || value === null)) {
499
491
  sig.value = value
500
492
  } else {
501
- signals[index] = toSignal(value)
493
+ signals[index] = toElement(value)
502
494
  }
503
495
  // Trigger parent signal on element assignment (look up when called)
504
496
  const parentSignal = arrayParentSignalMap.get(wrapped) || (wrapped as any)._parentSignal
@@ -520,9 +512,13 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
520
512
  }
521
513
  }
522
514
  return true
523
- } else if (prop === 'length') {
524
- signals.length = Number(value)
525
- // Trigger parent signal on length change (look up when called)
515
+ }
516
+ }
517
+ if (prop === 'length') {
518
+ const previousLength = signals.length
519
+ const result = Reflect.set(target, prop, value)
520
+ // Resizing adds or drops elements without going through a mutator, so it notifies like splice.
521
+ if (signals.length !== previousLength) {
526
522
  const parentSignal = arrayParentSignalMap.get(wrapped) || (wrapped as any)._parentSignal
527
523
  if (parentSignal) {
528
524
  // Notify subscribers directly since the array reference hasn't changed
@@ -541,8 +537,8 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
541
537
  ;(signal as any).__redrawCallback(parentSignal)
542
538
  }
543
539
  }
544
- return true
545
540
  }
541
+ return result
546
542
  }
547
543
  return Reflect.set(target, prop, value)
548
544
  },