@bitstillery/mithril 3.9.1 → 3.9.3

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
@@ -167,7 +167,7 @@ export default function router($window: any, mountRedraw: MountRedraw) {
167
167
  function getRedirectPath(redirectObj: RedirectObject): string {
168
168
  // First try our REDIRECT symbol
169
169
  if (REDIRECT in redirectObj) {
170
- return redirectObj[REDIRECT]
170
+ return redirectObj[REDIRECT]!
171
171
  }
172
172
  // Otherwise, check all symbol keys
173
173
  const symbolKeys = Object.getOwnPropertySymbols(redirectObj)
@@ -215,9 +215,9 @@ export default function router($window: any, mountRedraw: MountRedraw) {
215
215
  function loop(i: number) {
216
216
  if (!compiled) return
217
217
  for (; i < compiled.length; i++) {
218
- if (compiled[i].check(data)) {
219
- let payload = compiled[i].component
220
- const matchedRoute = compiled[i].route
218
+ if (compiled[i]!.check(data)) {
219
+ let payload = compiled[i]!.component
220
+ const matchedRoute = compiled[i]!.route
221
221
  const localComp = payload
222
222
  // Store the RouteResolver if payload has both onmatch and render
223
223
  // This allows us to preserve the resolver even after onmatch returns a component
@@ -237,7 +237,7 @@ export default function router($window: any, mountRedraw: MountRedraw) {
237
237
  // Handle redirect objects: explicit redirect signal
238
238
  if (isRedirect(comp)) {
239
239
  // Extract redirect target path
240
- const redirectPath = comp[REDIRECT]
240
+ const redirectPath = getRedirectPath(comp)
241
241
  const resolve = routeSetResolve
242
242
  routeSetResolve = null
243
243
  // Trigger navigation to redirect target; resolve original Promise when redirect completes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitstillery/mithril",
3
- "version": "3.9.1",
3
+ "version": "3.9.3",
4
4
  "description": "Mithril + Signals, Store and SSR",
5
5
  "license": "MIT",
6
6
  "author": "Bitstillery",
@@ -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/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/signal.ts CHANGED
@@ -188,7 +188,7 @@ export class ComputedSignal<T> extends Signal<T> {
188
188
  this._compute = compute
189
189
  }
190
190
 
191
- get value(): T {
191
+ override get value(): T {
192
192
  // Track access by other computed signals - this enables computed-to-computed dependency chains
193
193
  // When computed B accesses computed A, A should notify B when A's dependencies change
194
194
  if (currentEffect) {
@@ -259,7 +259,7 @@ export class ComputedSignal<T> extends Signal<T> {
259
259
  this._markDirty()
260
260
  }
261
261
 
262
- set value(_newValue: T) {
262
+ override set value(_newValue: T) {
263
263
  throw new Error('Computed signals are read-only')
264
264
  }
265
265
  }
package/state.ts CHANGED
@@ -60,12 +60,37 @@ function isState(value: any): boolean {
60
60
  return value && typeof value === 'object' && (value as any).__isState === true
61
61
  }
62
62
 
63
+ /**
64
+ * Built-ins whose methods need their internal slots. A proxy has none, and the get trap calls a
65
+ * function-valued property with the proxy as `this`, so a proxied Date or Map throws on every method
66
+ * call (and a Map's `get`/`set` read as a get/set descriptor). They are held as a signal's value
67
+ * instead, and replaced wholesale to notify.
68
+ */
69
+ function isOpaqueObject(value: object): boolean {
70
+ return (
71
+ value instanceof Date ||
72
+ value instanceof Map ||
73
+ value instanceof Set ||
74
+ value instanceof WeakMap ||
75
+ value instanceof WeakSet ||
76
+ value instanceof RegExp ||
77
+ value instanceof Promise ||
78
+ value instanceof ArrayBuffer ||
79
+ ArrayBuffer.isView(value)
80
+ )
81
+ }
82
+
63
83
  /**
64
84
  * Check if a value is a get/set descriptor object (like JavaScript property descriptors)
65
85
  * Used to detect computed properties defined as { get: () => T, set?: (value: T) => void }
66
86
  */
67
87
  function isGetSetDescriptor(value: any): boolean {
68
- return value && typeof value === 'object' && (typeof value.get === 'function' || typeof value.set === 'function')
88
+ return (
89
+ value &&
90
+ typeof value === 'object' &&
91
+ !isOpaqueObject(value) &&
92
+ (typeof value.get === 'function' || typeof value.set === 'function')
93
+ )
69
94
  }
70
95
 
71
96
  /**
@@ -203,7 +228,7 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
203
228
  parentSignalMap?: Map<string, Signal<any> | ComputedSignal<any>>,
204
229
  context?: InitContext,
205
230
  ): any {
206
- if (obj === null || typeof obj !== 'object') {
231
+ if (obj === null || typeof obj !== 'object' || isOpaqueObject(obj)) {
207
232
  return obj
208
233
  }
209
234
 
@@ -863,6 +888,48 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
863
888
  return wrapped
864
889
  }
865
890
 
891
+ /** The built-ins `isOpaqueObject` keeps out of the proxy: state holds them as they are. */
892
+ type OpaqueObject =
893
+ | Date
894
+ | Map<any, any>
895
+ | Set<any>
896
+ | WeakMap<any, any>
897
+ | WeakSet<any>
898
+ | RegExp
899
+ | Promise<any>
900
+ | ArrayBuffer
901
+ | ArrayBufferView
902
+
903
+ /**
904
+ * A value as state hands it out: a function reads as its computed result, an object (or array) as its
905
+ * own State. Distributes over a union, so a nullable object (`Foo | null`) is still a State with its
906
+ * `$` signals once it is set.
907
+ */
908
+ type StateValue<V> = V extends (...args: any[]) => infer R
909
+ ? R
910
+ : V extends OpaqueObject
911
+ ? V
912
+ : V extends Record<string, any>
913
+ ? State<V>
914
+ : V
915
+
916
+ /**
917
+ * A property as state hands it out. Unlike an array element it doesn't distribute over a union, so a
918
+ * nullable object (`Foo | null`) stays its plain shape and a plain `Foo` can be written to it, as in 3.9.
919
+ * A `{get, set}` descriptor reads as its computed value.
920
+ */
921
+ type StateProp<V> = [V] extends [never]
922
+ ? V
923
+ : [V] extends [(...args: any[]) => infer R]
924
+ ? R
925
+ : [V] extends [{get: () => infer R; set: (value: never) => void}]
926
+ ? R
927
+ : [V] extends [OpaqueObject]
928
+ ? V
929
+ : [V] extends [Record<string, any>]
930
+ ? State<V>
931
+ : V
932
+
866
933
  /**
867
934
  * Mapped type that adds $prop for each key, returning the Signal for that property.
868
935
  * - Primitives: $prop => Signal<T[K]>
@@ -874,24 +941,19 @@ export type StateSignals<T extends Record<string, any>> = {
874
941
  // lookup by a `string` key would read as `Value | Signal<Value>`.
875
942
  [K in keyof T as K extends string ? (string extends K ? never : `$${K}`) : never]: T[K] extends (...args: any[]) => infer R
876
943
  ? ComputedSignal<R>
877
- : T[K] extends object
878
- ? Signal<State<T[K]>>
879
- : Signal<T[K]>
944
+ : Signal<StateProp<T[K]>>
880
945
  }
881
- type StateElem<E> = E extends Record<string, any> ? State<E> : E
882
946
 
883
- export type StateArray<Elem> = Omit<Array<StateElem<Elem>>, 'fill' | 'push' | 'splice' | 'unshift'> & {
947
+ export type StateArray<Elem> = Omit<Array<StateValue<Elem>>, 'fill' | 'push' | 'splice' | 'unshift'> & {
884
948
  fill(value: Elem, start?: number, end?: number): StateArray<Elem>
885
949
  push(...items: Elem[]): number
886
- splice(start: number, deleteCount?: number, ...items: Elem[]): StateElem<Elem>[]
950
+ splice(start: number, deleteCount?: number, ...items: Elem[]): StateValue<Elem>[]
887
951
  unshift(...items: Elem[]): number
888
952
  }
889
953
 
890
954
  export type State<T extends Record<string, any>> = T extends (infer Elem)[]
891
955
  ? StateArray<Elem>
892
- : {
893
- [K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : T[K] extends Record<string, any> ? State<T[K]> : T[K]
894
- } & StateSignals<T>
956
+ : {[K in keyof T]: StateProp<T[K]>} & StateSignals<T>
895
957
 
896
958
  /**
897
959
  * A partial of a state shape at every depth, for the persistence tiers of a Store: each tier fills in
package/store.ts CHANGED
@@ -1,4 +1,5 @@
1
- import {allowComputed, state, State, updateStateRegistry, type DeepPartial} from './state'
1
+ import {allowComputed, state, updateStateRegistry} from './state'
2
+ import type {State, DeepPartial} from './state'
2
3
  import {serializeStore, deserializeStore} from './render/ssrState'
3
4
 
4
5
  // Helper function to restore computed properties (same as in ssrState.ts)
@@ -21,11 +22,11 @@ function restoreComputedProperties(state: State<any>, initial: any): void {
21
22
  const keys = prefix ? prefix.split('.').filter((k) => k) : []
22
23
  let targetState = target
23
24
  for (let i = 0; i < keys.length; i++) {
24
- if (!targetState || !targetState[keys[i]]) {
25
+ if (!targetState || !targetState[keys[i]!]) {
25
26
  // Nested state doesn't exist yet, skip
26
27
  return
27
28
  }
28
- targetState = targetState[keys[i]]
29
+ targetState = targetState[keys[i]!]
29
30
  }
30
31
  if (targetState) {
31
32
  // Clear any existing signal in signalMap so function is re-initialized as ComputedSignal
@@ -536,7 +537,7 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
536
537
  try {
537
538
  const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`))
538
539
  if (!match) return {}
539
- const parsed = JSON.parse(decodeURIComponent(match[1]))
540
+ const parsed = JSON.parse(decodeURIComponent(match[1]!))
540
541
  return parsed && typeof parsed === 'object' ? parsed : {}
541
542
  } catch {
542
543
  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')