@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 +18 -6
- package/index.ts +1 -1
- package/package.json +2 -2
- package/pathname/compileTemplate.ts +3 -2
- package/querystring/parse.ts +6 -5
- package/render/hyperscript.ts +7 -5
- package/render/render.ts +12 -10
- package/render/ssrState.ts +2 -2
- package/render/vnode.ts +15 -8
- package/server/logger.ts +1 -1
- package/server/session.ts +1 -1
- package/state.ts +26 -30
- package/store.ts +3 -3
- package/util/ssr.ts +1 -1
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:
|
|
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]
|
|
207
|
-
let payload = compiled[i]
|
|
208
|
-
const matchedRoute = compiled[i]
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/querystring/parse.ts
CHANGED
|
@@ -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]
|
|
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
|
-
|
|
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()
|
package/render/hyperscript.ts
CHANGED
|
@@ -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
|
package/render/ssrState.ts
CHANGED
|
@@ -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
|
|
33
|
-
oncreate
|
|
34
|
-
onbeforeupdate
|
|
35
|
-
onupdate
|
|
36
|
-
onbeforeremove
|
|
37
|
-
onremove
|
|
38
|
-
view
|
|
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
|
|
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
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
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]
|
|
201
|
+
lines.push(' '.repeat(i) + domElements[i]!.closeTag)
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
return lines.join('\n')
|