@bitstillery/mithril 3.9.0 → 3.9.2

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: {}
@@ -155,7 +167,7 @@ export default function router($window: any, mountRedraw: MountRedraw) {
155
167
  function getRedirectPath(redirectObj: RedirectObject): string {
156
168
  // First try our REDIRECT symbol
157
169
  if (REDIRECT in redirectObj) {
158
- return redirectObj[REDIRECT]
170
+ return redirectObj[REDIRECT]!
159
171
  }
160
172
  // Otherwise, check all symbol keys
161
173
  const symbolKeys = Object.getOwnPropertySymbols(redirectObj)
@@ -203,9 +215,9 @@ export default function router($window: any, mountRedraw: MountRedraw) {
203
215
  function loop(i: number) {
204
216
  if (!compiled) return
205
217
  for (; i < compiled.length; i++) {
206
- if (compiled[i].check(data)) {
207
- let payload = compiled[i].component
208
- const matchedRoute = compiled[i].route
218
+ if (compiled[i]!.check(data)) {
219
+ let payload = compiled[i]!.component
220
+ const matchedRoute = compiled[i]!.route
209
221
  const localComp = payload
210
222
  // Store the RouteResolver if payload has both onmatch and render
211
223
  // This allows us to preserve the resolver even after onmatch returns a component
@@ -225,7 +237,7 @@ export default function router($window: any, mountRedraw: MountRedraw) {
225
237
  // Handle redirect objects: explicit redirect signal
226
238
  if (isRedirect(comp)) {
227
239
  // Extract redirect target path
228
- const redirectPath = comp[REDIRECT]
240
+ const redirectPath = getRedirectPath(comp)
229
241
  const resolve = routeSetResolve
230
242
  routeSetResolve = null
231
243
  // Trigger navigation to redirect target; resolve original Promise when redirect completes
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.2",
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",
@@ -38,14 +38,15 @@ export default function compileTemplate(template: string): CompiledTemplate {
38
38
  // First, check the params. Usually, there isn't any, and it's just
39
39
  // checking a static set.
40
40
  for (let i = 0; i < templateKeys.length; i++) {
41
- if (templateData.params[templateKeys[i]] !== data.params[templateKeys[i]]) return false
41
+ if (templateData.params[templateKeys[i]!] !== data.params[templateKeys[i]!]) return false
42
42
  }
43
43
  // If no interpolations exist, let's skip all the ceremony
44
44
  if (!keys.length) return regexp.test(data.path)
45
45
  const values = regexp.exec(data.path)
46
46
  if (values == null) return false
47
47
  for (let i = 0; i < keys.length; i++) {
48
- data.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1])
48
+ // Every key is a non-optional capture group, so a match fills all of them.
49
+ data.params[keys[i]!.k] = keys[i]!.r ? values[i + 1] : decodeURIComponent(values[i + 1]!)
49
50
  }
50
51
  return true
51
52
  }
@@ -8,9 +8,9 @@ export default function parseQueryString(string: string | null | undefined): Rec
8
8
  const counters: Record<string, number> = {}
9
9
  const data: Record<string, any> = {}
10
10
  for (let i = 0; i < entries.length; i++) {
11
- const entry = entries[i].split('=')
12
- const key = decodeURIComponentSafe(entry[0])
13
- let value: any = entry.length === 2 ? decodeURIComponentSafe(entry[1]) : ''
11
+ const entry = entries[i]!.split('=')
12
+ const key = decodeURIComponentSafe(entry[0]!)
13
+ let value: any = entry.length === 2 ? decodeURIComponentSafe(entry[1]!) : ''
14
14
 
15
15
  if (value === 'true') value = true
16
16
  else if (value === 'false') value = false
@@ -19,9 +19,10 @@ export default function parseQueryString(string: string | null | undefined): Rec
19
19
  let cursor: any = data
20
20
  if (key.indexOf('[') > -1) levels.pop()
21
21
  for (let j = 0; j < levels.length; j++) {
22
- const level = levels[j]
22
+ const level = levels[j]!
23
23
  const nextLevel = levels[j + 1]
24
- const isNumber = nextLevel == '' || !isNaN(parseInt(nextLevel, 10))
24
+ // Past the last level nextLevel is undefined, which parseInt reads as NaN.
25
+ const isNumber = nextLevel == '' || !isNaN(parseInt(nextLevel as string, 10))
25
26
  let finalLevel: string | number
26
27
  if (level === '') {
27
28
  const key = levels.slice(0, j).join()
@@ -31,25 +31,27 @@ function isFormAttributeKey(key: string): boolean {
31
31
  return key === 'value' || key === 'checked' || key === 'selectedIndex' || key === 'selected'
32
32
  }
33
33
 
34
+ // A match fills groups 1-2 (tag/#id/.class) or groups 3-4 ([attr]), never both; group 6
35
+ // is absent for a bare `[attr]`, so a bare `[class]` joins in as an empty class.
34
36
  function compileSelector(selector: string): {tag: string; attrs: Record<string, any>; is?: string} {
35
37
  let match: RegExpExecArray | null
36
38
  let tag = 'div'
37
- const classes: string[] = []
39
+ const classes: (string | undefined)[] = []
38
40
  let attrs: Record<string, any> = {}
39
41
  let isStatic = true
40
42
  while ((match = selectorParser.exec(selector)) !== null) {
41
43
  const type = match[1]
42
44
  const value = match[2]
43
- if (type === '' && value !== '') tag = value
45
+ if (type === '' && value !== '') tag = value!
44
46
  else if (type === '#') attrs.id = value
45
47
  else if (type === '.') classes.push(value)
46
- else if (match[3][0] === '[') {
48
+ else if (match[3]![0] === '[') {
47
49
  let attrValue = match[6]
48
50
  if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, '$1').replace(/\\\\/g, '\\')
49
51
  if (match[4] === 'class') classes.push(attrValue)
50
52
  else {
51
- attrs[match[4]] = attrValue === '' ? attrValue : attrValue || true
52
- if (isFormAttributeKey(match[4])) isStatic = false
53
+ attrs[match[4]!] = attrValue === '' ? attrValue : attrValue || true
54
+ if (isFormAttributeKey(match[4]!)) isStatic = false
53
55
  }
54
56
  }
55
57
  }
package/render/render.ts CHANGED
@@ -895,16 +895,18 @@ export default function renderFactory() {
895
895
  // of the items that are part of the longest increasing
896
896
  // subsequence
897
897
  const lisTemp: number[] = []
898
+ // Every index read below is in range: i < a.length, `result` is never empty and holds
899
+ // indices into `a`, and `lisTemp` mirrors `a`'s length.
898
900
  function makeLisIndices(a: number[]): number[] {
899
901
  const result = [0]
900
902
  let u = 0,
901
903
  v = 0
902
904
  const il = (lisTemp.length = a.length)
903
- for (let i = 0; i < il; i++) lisTemp[i] = a[i]
905
+ for (let i = 0; i < il; i++) lisTemp[i] = a[i]!
904
906
  for (let i = 0; i < il; ++i) {
905
907
  if (a[i] === -1) continue
906
- const j = result[result.length - 1]
907
- if (a[j] < a[i]) {
908
+ const j = result[result.length - 1]!
909
+ if (a[j]! < a[i]!) {
908
910
  lisTemp[i] = j
909
911
  result.push(i)
910
912
  continue
@@ -915,22 +917,22 @@ export default function renderFactory() {
915
917
  // Fast integer average without overflow.
916
918
 
917
919
  const c = (u >>> 1) + (v >>> 1) + (u & v & 1)
918
- if (a[result[c]] < a[i]) {
920
+ if (a[result[c]!]! < a[i]!) {
919
921
  u = c + 1
920
922
  } else {
921
923
  v = c
922
924
  }
923
925
  }
924
- if (a[i] < a[result[u]]) {
925
- if (u > 0) lisTemp[i] = result[u - 1]
926
+ if (a[i]! < a[result[u]!]!) {
927
+ if (u > 0) lisTemp[i] = result[u - 1]!
926
928
  result[u] = i
927
929
  }
928
930
  }
929
931
  u = result.length
930
- v = result[u - 1]
932
+ v = result[u - 1]!
931
933
  while (u-- > 0) {
932
934
  result[u] = v
933
- v = lisTemp[v]
935
+ v = lisTemp[v]!
934
936
  }
935
937
  lisTemp.length = 0
936
938
  return result
@@ -1453,14 +1455,14 @@ export default function renderFactory() {
1453
1455
  false,
1454
1456
  )
1455
1457
  // Execute hooks from override render
1456
- for (let i = 0; i < overrideHooks.length; i++) overrideHooks[i]()
1458
+ for (let i = 0; i < overrideHooks.length; i++) overrideHooks[i]!()
1457
1459
  }
1458
1460
 
1459
1461
  ;(dom as any).vnodes = normalized
1460
1462
  // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
1461
1463
  if (active != null && activeElement(dom) !== active && typeof (active as any).focus === 'function')
1462
1464
  (active as any).focus()
1463
- for (let i = 0; i < hooks.length; i++) hooks[i]()
1465
+ for (let i = 0; i < hooks.length; i++) hooks[i]!()
1464
1466
  } finally {
1465
1467
  currentRedraw = prevRedraw
1466
1468
  currentDOM = prevDOM
@@ -306,11 +306,11 @@ function restoreComputedProperties(state: State<any>, initial: any): void {
306
306
  const keys = prefix ? prefix.split('.').filter((k) => k) : []
307
307
  let targetState = target
308
308
  for (let i = 0; i < keys.length; i++) {
309
- if (!targetState[keys[i]]) {
309
+ if (!targetState[keys[i]!]) {
310
310
  // Nested state doesn't exist yet, skip
311
311
  return
312
312
  }
313
- targetState = targetState[keys[i]]
313
+ targetState = targetState[keys[i]!]
314
314
  }
315
315
  // Clear any existing signal in signalMap so function is re-initialized as ComputedSignal
316
316
  if (targetState && typeof targetState === 'object' && (targetState as any).__isState) {
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/server/logger.ts CHANGED
@@ -96,7 +96,7 @@ function getColorByHash(text: string): string {
96
96
  colors.cyan,
97
97
  ]
98
98
  const hash = hashString(text)
99
- return colorOptions[hash % colorOptions.length]
99
+ return colorOptions[hash % colorOptions.length]!
100
100
  }
101
101
 
102
102
  const textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null
package/server/session.ts CHANGED
@@ -83,5 +83,5 @@ export class MemorySessionStore implements SessionStore {
83
83
  export function extractSessionId(req: Request): string | null {
84
84
  const cookies = req.headers.get('cookie') || ''
85
85
  const sessionIdMatch = cookies.match(/sessionId=([^;]+)/)
86
- return sessionIdMatch ? sessionIdMatch[1] : null
86
+ return sessionIdMatch ? sessionIdMatch[1]! : null
87
87
  }
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
  },
package/store.ts CHANGED
@@ -21,11 +21,11 @@ function restoreComputedProperties(state: State<any>, initial: any): void {
21
21
  const keys = prefix ? prefix.split('.').filter((k) => k) : []
22
22
  let targetState = target
23
23
  for (let i = 0; i < keys.length; i++) {
24
- if (!targetState || !targetState[keys[i]]) {
24
+ if (!targetState || !targetState[keys[i]!]) {
25
25
  // Nested state doesn't exist yet, skip
26
26
  return
27
27
  }
28
- targetState = targetState[keys[i]]
28
+ targetState = targetState[keys[i]!]
29
29
  }
30
30
  if (targetState) {
31
31
  // Clear any existing signal in signalMap so function is re-initialized as ComputedSignal
@@ -536,7 +536,7 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
536
536
  try {
537
537
  const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`))
538
538
  if (!match) return {}
539
- const parsed = JSON.parse(decodeURIComponent(match[1]))
539
+ const parsed = JSON.parse(decodeURIComponent(match[1]!))
540
540
  return parsed && typeof parsed === 'object' ? parsed : {}
541
541
  } catch {
542
542
  return {}
package/util/ssr.ts CHANGED
@@ -198,7 +198,7 @@ function formatCombinedStructure(parent: Element | Node | null, vnode: any, maxP
198
198
 
199
199
  // Closing tags for DOM parents (in reverse order)
200
200
  for (let i = domElements.length - 1; i >= 0; i--) {
201
- lines.push(' '.repeat(i) + domElements[i].closeTag)
201
+ lines.push(' '.repeat(i) + domElements[i]!.closeTag)
202
202
  }
203
203
 
204
204
  return lines.join('\n')