kiru 1.2.0 → 1.2.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/dist/appHandle.d.ts.map +1 -1
- package/dist/appHandle.js +4 -5
- package/dist/appHandle.js.map +1 -1
- package/dist/components/derive.d.ts +1 -1
- package/dist/components/derive.d.ts.map +1 -1
- package/dist/components/derive.js +1 -1
- package/dist/components/derive.js.map +1 -1
- package/dist/components/errorBoundary.d.ts +1 -1
- package/dist/components/errorBoundary.js +1 -1
- package/dist/components/for.d.ts +1 -1
- package/dist/components/for.d.ts.map +1 -1
- package/dist/components/for.js +1 -1
- package/dist/components/lazy.d.ts +1 -1
- package/dist/components/lazy.js +1 -1
- package/dist/components/portal.d.ts +1 -1
- package/dist/components/portal.js +1 -1
- package/dist/components/show.d.ts +1 -1
- package/dist/components/show.js +1 -1
- package/dist/components/transition.d.ts +1 -1
- package/dist/components/transition.js +1 -1
- package/dist/dom/nodes.d.ts.map +1 -1
- package/dist/dom/nodes.js +5 -1
- package/dist/dom/nodes.js.map +1 -1
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +23 -4
- package/dist/dom/props.js.map +1 -1
- package/dist/hooks/setup.d.ts +1 -1
- package/dist/hooks/setup.d.ts.map +1 -1
- package/dist/hooks/setup.js +110 -6
- package/dist/hooks/setup.js.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +2 -3
- package/dist/scheduler.js.map +1 -1
- package/dist/signals/base.d.ts.map +1 -1
- package/dist/signals/base.js +23 -4
- package/dist/signals/base.js.map +1 -1
- package/dist/signals/computed.d.ts.map +1 -1
- package/dist/signals/computed.js +9 -1
- package/dist/signals/computed.js.map +1 -1
- package/dist/utils/vdom.d.ts +2 -1
- package/dist/utils/vdom.d.ts.map +1 -1
- package/dist/utils/vdom.js +4 -1
- package/dist/utils/vdom.js.map +1 -1
- package/package.json +1 -1
- package/src/appHandle.ts +6 -7
- package/src/components/derive.ts +15 -14
- package/src/components/errorBoundary.ts +1 -1
- package/src/components/for.ts +34 -34
- package/src/components/lazy.ts +1 -1
- package/src/components/portal.ts +1 -1
- package/src/components/show.ts +32 -32
- package/src/components/transition.ts +1 -1
- package/src/dom/nodes.ts +5 -2
- package/src/dom/props.ts +28 -3
- package/src/hooks/setup.ts +143 -9
- package/src/scheduler.ts +2 -3
- package/src/signals/base.ts +21 -5
- package/src/signals/computed.ts +9 -1
- package/src/utils/vdom.ts +5 -0
package/src/dom/nodes.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { hydrationStack } from "../hydration.js"
|
|
|
5
5
|
import {
|
|
6
6
|
getVNodeApp,
|
|
7
7
|
isValidTextChild,
|
|
8
|
+
latest,
|
|
8
9
|
registerVNodeCleanup,
|
|
9
10
|
} from "../utils/index.js"
|
|
10
11
|
import { KiruError } from "../error.js"
|
|
@@ -27,8 +28,8 @@ function createDom(vNode: DomVNode): SomeDom {
|
|
|
27
28
|
t == "#text"
|
|
28
29
|
? createTextNode(vNode)
|
|
29
30
|
: svgTags.has(t)
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
? document.createElementNS("http://www.w3.org/2000/svg", t)
|
|
32
|
+
: document.createElement(t)
|
|
32
33
|
|
|
33
34
|
return dom
|
|
34
35
|
}
|
|
@@ -175,6 +176,7 @@ function getOrCreateTextNode(vNode: VNode): MaybeDom {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
function subTextNode(vNode: VNode, textNode: Text, signal: Signal<string>) {
|
|
179
|
+
if (__DEV__) signal = latest(signal)
|
|
178
180
|
const cleanup = signal.subscribe((value, prev) => {
|
|
179
181
|
if (value === prev) return
|
|
180
182
|
textNode.nodeValue = value
|
|
@@ -198,6 +200,7 @@ function createTextNode(vNode: VNode): Text {
|
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
function createSignalTextNode(vNode: VNode, nodeValue: Signal<string>): Text {
|
|
203
|
+
if (__DEV__) nodeValue = latest(nodeValue) as Signal<string>
|
|
201
204
|
const value = nodeValue.peek() ?? ""
|
|
202
205
|
const textNode = document.createTextNode(value)
|
|
203
206
|
subTextNode(vNode, textNode, nodeValue)
|
package/src/dom/props.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getVNodeApp,
|
|
4
4
|
setRef,
|
|
5
5
|
registerVNodeCleanup,
|
|
6
|
+
latest,
|
|
6
7
|
} from "../utils/index.js"
|
|
7
8
|
import { Signal } from "../signals/base.js"
|
|
8
9
|
import { unwrap } from "../signals/utils.js"
|
|
@@ -47,9 +48,33 @@ function updateDomProps(vNode: DomVNode) {
|
|
|
47
48
|
const nextProps = props ?? {}
|
|
48
49
|
|
|
49
50
|
if (isTextNode(dom)) {
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
51
|
+
let nextVal = nextProps.nodeValue
|
|
52
|
+
if (__DEV__ && Signal.isSignal(nextVal)) nextVal = latest(nextVal)
|
|
53
|
+
|
|
54
|
+
if (!Signal.isSignal(nextVal)) {
|
|
55
|
+
if (dom.nodeValue !== nextVal) {
|
|
56
|
+
dom.nodeValue = nextVal
|
|
57
|
+
}
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (prevProps.nodeValue === nextVal) return
|
|
61
|
+
dom.nodeValue = String(nextVal.peek() ?? "")
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
cleanups?.nodeValue?.()
|
|
64
|
+
registerVNodeCleanup(
|
|
65
|
+
vNode,
|
|
66
|
+
"nodeValue",
|
|
67
|
+
nextVal.subscribe((value, prev) => {
|
|
68
|
+
if (value === prev) return
|
|
69
|
+
dom.nodeValue = String(value ?? "")
|
|
70
|
+
if (isBrowser) {
|
|
71
|
+
window.__kiru?.profilingContext?.emit(
|
|
72
|
+
"signalTextUpdate",
|
|
73
|
+
getVNodeApp(vNode)!
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
)
|
|
53
78
|
}
|
|
54
79
|
return
|
|
55
80
|
}
|
package/src/hooks/setup.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { signal, Signal } from "../signals/base.js"
|
|
2
2
|
import { createVNodeId } from "../utils/vdom.js"
|
|
3
3
|
import { __DEV__ } from "../env.js"
|
|
4
4
|
import { node, setups } from "../globals.js"
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
tracking,
|
|
7
|
+
type TrackingStackObservations,
|
|
8
|
+
} from "../signals/tracking.js"
|
|
9
|
+
import { registerVNodeCleanup } from "../utils/index.js"
|
|
6
10
|
|
|
7
11
|
export interface Setup<Props extends {}> {
|
|
8
12
|
readonly derive: <T>(
|
|
@@ -36,7 +40,6 @@ export function setup<Props extends {}>(): Setup<Props> {
|
|
|
36
40
|
setups.set(vNode, setup)
|
|
37
41
|
return setup
|
|
38
42
|
}
|
|
39
|
-
|
|
40
43
|
function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
|
|
41
44
|
let id: Signal<string>
|
|
42
45
|
|
|
@@ -45,12 +48,89 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
|
|
|
45
48
|
|
|
46
49
|
let prevIndex = -1
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Always points at latest props (updated in propSync) so selector and subs see current props
|
|
52
|
+
const currentProps = { current: { ...vNode.props } as InferredProps }
|
|
53
|
+
const deriveCleanups: Array<() => void> = []
|
|
54
|
+
|
|
55
|
+
type DeriveEntry = { run: () => void; accessedPaths: Set<string> }
|
|
56
|
+
const deriveEntries: DeriveEntry[] = []
|
|
57
|
+
|
|
58
|
+
propSyncs.push((p) => {
|
|
59
|
+
const old = currentProps.current as Record<string, unknown>
|
|
60
|
+
const skip = new Set<DeriveEntry>()
|
|
61
|
+
for (const entry of deriveEntries) {
|
|
62
|
+
if (
|
|
63
|
+
entry.accessedPaths.size > 0 &&
|
|
64
|
+
propsUnchangedAtPaths(
|
|
65
|
+
old,
|
|
66
|
+
p as Record<string, unknown>,
|
|
67
|
+
entry.accessedPaths
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
skip.add(entry)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
currentProps.current = p
|
|
74
|
+
for (const entry of deriveEntries) {
|
|
75
|
+
if (!skip.has(entry)) entry.run()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
registerVNodeCleanup(vNode, "vnode:setup", () => {
|
|
80
|
+
for (const cleanup of deriveCleanups) cleanup()
|
|
81
|
+
setups.delete(vNode)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const setupResult: Setup<Props> = {
|
|
85
|
+
derive<T>(
|
|
86
|
+
selector: (props: Props extends Kiru.FC<infer P> ? P : Props) => T
|
|
87
|
+
) {
|
|
88
|
+
const resultSig = signal(undefined!) as Signal<T>
|
|
89
|
+
const unsubs = new Map<string, () => void>()
|
|
90
|
+
const accessedPaths = new Set<string>()
|
|
91
|
+
|
|
92
|
+
function sync() {
|
|
93
|
+
accessedPaths.clear()
|
|
94
|
+
const propsProxy = createPropsProxy(
|
|
95
|
+
currentProps.current as Record<string, unknown>,
|
|
96
|
+
accessedPaths
|
|
97
|
+
) as InferredProps
|
|
98
|
+
const observations: TrackingStackObservations = new Map()
|
|
99
|
+
tracking.stack.push(observations)
|
|
100
|
+
const value = selector(propsProxy)
|
|
101
|
+
tracking.stack.pop()
|
|
102
|
+
// Always assign and notify so the component re-renders when the derived value changes
|
|
103
|
+
// (e.g. when parent passes a different signal ref like toggle switching count/double).
|
|
104
|
+
resultSig.value = value
|
|
105
|
+
|
|
106
|
+
for (const [sid, unsub] of unsubs) {
|
|
107
|
+
if (!observations.has(sid)) {
|
|
108
|
+
unsub()
|
|
109
|
+
unsubs.delete(sid)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const [sid, observedSig] of observations) {
|
|
113
|
+
if (!unsubs.has(sid)) {
|
|
114
|
+
try {
|
|
115
|
+
unsubs.set(sid, observedSig.subscribe(sync))
|
|
116
|
+
} catch {
|
|
117
|
+
// Signal may be disposed after HMR; skip subscribing
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
sync()
|
|
124
|
+
const entry: DeriveEntry = { run: sync, accessedPaths }
|
|
125
|
+
deriveEntries.push(entry)
|
|
126
|
+
deriveCleanups.push(() => {
|
|
127
|
+
unsubs.forEach((u) => u())
|
|
128
|
+
unsubs.clear()
|
|
129
|
+
const i = deriveEntries.indexOf(entry)
|
|
130
|
+
if (i !== -1) deriveEntries.splice(i, 1)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return resultSig
|
|
54
134
|
},
|
|
55
135
|
get id() {
|
|
56
136
|
if (!id) {
|
|
@@ -67,4 +147,58 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
|
|
|
67
147
|
return id
|
|
68
148
|
},
|
|
69
149
|
}
|
|
150
|
+
return setupResult
|
|
151
|
+
}
|
|
152
|
+
function propsUnchangedAtPaths(
|
|
153
|
+
oldProps: Record<string, unknown>,
|
|
154
|
+
newProps: Record<string, unknown>,
|
|
155
|
+
paths: Set<string>
|
|
156
|
+
): boolean {
|
|
157
|
+
for (const path of paths) {
|
|
158
|
+
if (!Object.is(getAtPath(oldProps, path), getAtPath(newProps, path))) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getAtPath(obj: Record<string, unknown>, path: string): unknown {
|
|
166
|
+
let cur: unknown = obj
|
|
167
|
+
for (const key of path.split(".")) {
|
|
168
|
+
if (cur == null || typeof cur !== "object") return undefined
|
|
169
|
+
cur = (cur as Record<string, unknown>)[key]
|
|
170
|
+
}
|
|
171
|
+
return cur
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Proxy that records paths and wraps signals. We only add to accessedPaths when
|
|
176
|
+
* we hit a signal (the leaf we subscribe to), so propSync skip only compares
|
|
177
|
+
* signal refs. Container objects (e.g. "data") are new every render and would
|
|
178
|
+
* always fail the skip.
|
|
179
|
+
*/
|
|
180
|
+
function createPropsProxy<P extends Record<string, unknown>>(
|
|
181
|
+
props: P,
|
|
182
|
+
accessedPaths: Set<string>,
|
|
183
|
+
pathPrefix?: string
|
|
184
|
+
): P {
|
|
185
|
+
return new Proxy(props, {
|
|
186
|
+
get(holder, key: string) {
|
|
187
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key
|
|
188
|
+
const v = holder[key]
|
|
189
|
+
if (Signal.isSignal(v)) {
|
|
190
|
+
accessedPaths.add(path) // only record path for signal leaves
|
|
191
|
+
return v
|
|
192
|
+
}
|
|
193
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
194
|
+
return createPropsProxy(
|
|
195
|
+
v as Record<string, unknown>,
|
|
196
|
+
accessedPaths,
|
|
197
|
+
path
|
|
198
|
+
) as P[keyof P]
|
|
199
|
+
}
|
|
200
|
+
accessedPaths.add(path) // primitive leaf
|
|
201
|
+
return v
|
|
202
|
+
},
|
|
203
|
+
}) as P
|
|
70
204
|
}
|
package/src/scheduler.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
findParentErrorBoundary,
|
|
23
23
|
call,
|
|
24
24
|
propsChanged,
|
|
25
|
+
depthSort,
|
|
25
26
|
} from "./utils/index.js"
|
|
26
27
|
import { __DEV__ } from "./env.js"
|
|
27
28
|
import { KiruError } from "./error.js"
|
|
@@ -129,8 +130,6 @@ function queueDelete(vNode: VNode): void {
|
|
|
129
130
|
deletions.push(vNode)
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
const depthSort = (a: VNode, b: VNode): number => b.depth - a.depth
|
|
133
|
-
|
|
134
133
|
let currentWorkRoot: VNode | null = null
|
|
135
134
|
|
|
136
135
|
function doWork(): void {
|
|
@@ -402,7 +401,7 @@ function renderFunctionComponent(
|
|
|
402
401
|
|
|
403
402
|
let newChild = latest(type)(props)
|
|
404
403
|
if (typeof newChild === "function") {
|
|
405
|
-
vNode.subs?.forEach(call) // unsub from signals
|
|
404
|
+
vNode.subs?.forEach(call) // unsub from signals observed during setup
|
|
406
405
|
vNode.render = newChild
|
|
407
406
|
if (shouldSyncProps) {
|
|
408
407
|
const p = { ...props }
|
package/src/signals/base.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
generateRandomID,
|
|
6
6
|
registerVNodeCleanup,
|
|
7
7
|
} from "../utils/index.js"
|
|
8
|
-
import { $HMR_ACCEPT, $SIGNAL } from "../constants.js"
|
|
8
|
+
import { $DEV_FILE_LINK, $HMR_ACCEPT, $SIGNAL } from "../constants.js"
|
|
9
9
|
import { __DEV__, isBrowser } from "../env.js"
|
|
10
10
|
import { node } from "../globals.js"
|
|
11
11
|
import { requestUpdate } from "../scheduler.js"
|
|
@@ -120,9 +120,19 @@ export class Signal<T> {
|
|
|
120
120
|
|
|
121
121
|
subscribe(cb: (state: T, prevState?: T) => void): () => void {
|
|
122
122
|
if (__DEV__) {
|
|
123
|
-
const
|
|
124
|
-
subs
|
|
125
|
-
|
|
123
|
+
const tgt = latest(this)
|
|
124
|
+
const subs = signalSubsMap.get(tgt.$id)!
|
|
125
|
+
if (__DEV__ && !subs) {
|
|
126
|
+
const name = tgt.displayName ?? tgt.$id
|
|
127
|
+
let message = `Attempting to subscribe to a signal that has been disposed: ${name}`
|
|
128
|
+
if ($DEV_FILE_LINK in tgt) {
|
|
129
|
+
message += `\nFile: ${tgt[$DEV_FILE_LINK]}`
|
|
130
|
+
}
|
|
131
|
+
message += `\nInitial value: ${tgt.$initialValue}`
|
|
132
|
+
throw new Error(message)
|
|
133
|
+
}
|
|
134
|
+
subs.add(cb)
|
|
135
|
+
return () => subs.delete(cb)
|
|
126
136
|
}
|
|
127
137
|
this.$subs!.add(cb)
|
|
128
138
|
return () => this.$subs!.delete(cb)
|
|
@@ -130,7 +140,8 @@ export class Signal<T> {
|
|
|
130
140
|
|
|
131
141
|
notify(filter?: (sub: SignalSubscriber) => boolean) {
|
|
132
142
|
if (__DEV__) {
|
|
133
|
-
|
|
143
|
+
const tgt = latest(this)
|
|
144
|
+
return signalSubsMap.get(tgt.$id)?.forEach((sub) => {
|
|
134
145
|
if (filter && !filter(sub)) return
|
|
135
146
|
const { $value, $prevValue } = latest(this)
|
|
136
147
|
return sub($value, $prevValue)
|
|
@@ -148,12 +159,14 @@ export class Signal<T> {
|
|
|
148
159
|
|
|
149
160
|
static subscribers(signal: Signal<any>) {
|
|
150
161
|
if (__DEV__) {
|
|
162
|
+
signal = latest(signal)
|
|
151
163
|
return signalSubsMap.get(signal.$id)!
|
|
152
164
|
}
|
|
153
165
|
return signal.$subs
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
static makeReadonly<T>(signal: Signal<T>): ReadonlySignal<T> {
|
|
169
|
+
if (__DEV__) signal = latest(signal)
|
|
157
170
|
const desc = Object.getOwnPropertyDescriptor(signal, "value")
|
|
158
171
|
if (desc && !desc.writable) return signal
|
|
159
172
|
return Object.defineProperty(signal, "value", {
|
|
@@ -166,6 +179,7 @@ export class Signal<T> {
|
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
static makeWritable<T>(signal: Signal<T>): Signal<T> {
|
|
182
|
+
if (__DEV__) signal = latest(signal)
|
|
169
183
|
const desc = Object.getOwnPropertyDescriptor(signal, "value")
|
|
170
184
|
if (desc && desc.writable) return signal
|
|
171
185
|
return Object.defineProperty(signal, "value", {
|
|
@@ -183,6 +197,7 @@ export class Signal<T> {
|
|
|
183
197
|
|
|
184
198
|
static entangle<T>(signal: Signal<T>) {
|
|
185
199
|
if (tracking.enabled === false) return
|
|
200
|
+
if (__DEV__) signal = latest(signal)
|
|
186
201
|
|
|
187
202
|
const vNode = node.current
|
|
188
203
|
const trackedSignalObservations = tracking.current()
|
|
@@ -200,6 +215,7 @@ export class Signal<T> {
|
|
|
200
215
|
static dispose(signal: Signal<any>) {
|
|
201
216
|
signal.$isDisposed = true
|
|
202
217
|
if (__DEV__) {
|
|
218
|
+
signal = latest(signal)
|
|
203
219
|
signalSubsMap.delete(signal.$id)
|
|
204
220
|
if (isBrowser) window.__kiru.devtools?.untrack(signal)
|
|
205
221
|
return
|
package/src/signals/computed.ts
CHANGED
|
@@ -25,8 +25,16 @@ export class ComputedSignal<T> extends Signal<T> {
|
|
|
25
25
|
},
|
|
26
26
|
inject: (prev) => {
|
|
27
27
|
inject(prev)
|
|
28
|
+
// Stop any pending reactions on the previous instance and mark
|
|
29
|
+
// this computed as dirty so it will recompute with the latest
|
|
30
|
+
// dependencies after HMR.
|
|
28
31
|
ComputedSignal.stop(prev)
|
|
29
|
-
this.$isDirty =
|
|
32
|
+
this.$isDirty = true
|
|
33
|
+
// Force a recompute immediately so any active subscribers (like
|
|
34
|
+
// text nodes bound to a computed signal) see the updated value
|
|
35
|
+
// after a hot reload where only its dependencies changed.
|
|
36
|
+
ComputedSignal.run(this)
|
|
37
|
+
this.notify()
|
|
30
38
|
},
|
|
31
39
|
destroy: () => {},
|
|
32
40
|
} satisfies HMRAccept<ComputedSignal<T>>
|
package/src/utils/vdom.ts
CHANGED
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
createVNodeId,
|
|
35
35
|
registerVNodeCleanup,
|
|
36
36
|
propsChanged,
|
|
37
|
+
depthSort,
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function cloneElement(vNode: Kiru.VNode): Kiru.Element {
|
|
@@ -219,3 +220,7 @@ function propsChanged(
|
|
|
219
220
|
}
|
|
220
221
|
return false
|
|
221
222
|
}
|
|
223
|
+
|
|
224
|
+
function depthSort(a: Kiru.VNode, b: Kiru.VNode): number {
|
|
225
|
+
return a.depth - b.depth
|
|
226
|
+
}
|