Skip to content

Commit 3e9765b

Browse files
Merge pull request #428 from preactjs/inspect-signal
Add basic support for Signals
2 parents 56deaf8 + 43eec35 commit 3e9765b

File tree

19 files changed

+397
-97
lines changed

19 files changed

+397
-97
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@parcel/css": "^1.12.2",
5151
"@playwright/test": "^1.25.1",
5252
"@preact/preset-vite": "^2.3.0",
53-
"@preact/signals": "^0.0.3",
53+
"@preact/signals": "^1.0.1",
5454
"@testing-library/preact": "^2.0.0",
5555
"@types/archiver": "^5.3.1",
5656
"@types/babel__core": "^7.1.19",

src/adapter/adapter/filter.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ export interface RawFilterState {
77
dom: boolean;
88
hoc?: boolean;
99
root?: boolean;
10+
textSignal?: boolean;
1011
};
1112
}
1213

13-
export type TypeFilterValue = "dom" | "fragment" | "hoc" | "root";
14+
export type TypeFilterValue =
15+
| "dom"
16+
| "fragment"
17+
| "hoc"
18+
| "root"
19+
| "textSignal";
1420

1521
export interface FilterState {
1622
regex: RegExp[];
@@ -31,7 +37,7 @@ export type Filter = RegexFilter | TypeFilter;
3137

3238
export const DEFAULT_FIlTERS: FilterState = {
3339
regex: [],
34-
type: new Set(["dom", "fragment", "root", "hoc"]),
40+
type: new Set(["dom", "fragment", "root", "hoc", "textSignal"]),
3541
};
3642

3743
export function parseFilters(raw: RawFilterState): FilterState {
@@ -40,6 +46,7 @@ export function parseFilters(raw: RawFilterState): FilterState {
4046
if (raw.type.dom) type.add("dom");
4147
if (raw.type.hoc) type.add("hoc");
4248
if (raw.type.root) type.add("root");
49+
if (raw.type.textSignal) type.add("textSignal");
4350

4451
return {
4552
regex: raw.regex.filter(x => x.enabled).map(x => new RegExp(x.value, "gi")),

src/adapter/shared/inspectVNode.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { cleanContext, cleanProps, serialize } from "./serialize";
66
import { PreactBindings, SharedVNode } from "./bindings";
77
import { InspectData } from "../adapter/adapter";
88
import { ID } from "../../view/store/types";
9+
import { getSignalTextName } from "./utils";
910

1011
/**
1112
* Serialize all properties/attributes of a `VNode` like `props`, `context`,
@@ -33,7 +34,11 @@ export function inspectVNode<T extends SharedVNode>(
3334
c.state != null &&
3435
Object.keys(c.state).length > 0;
3536

36-
const hasHooks = c != null && bindings.getComponentHooks(vnode) != null;
37+
const isSignalTextNode =
38+
typeof vnode.type === "function" && vnode.type.displayName === "_st";
39+
40+
const hasHooks =
41+
c != null && !isSignalTextNode && bindings.getComponentHooks(vnode) != null;
3742
const hooks =
3843
supportsHooks && hasHooks
3944
? inspectHooks(config, options, vnode, bindings)
@@ -69,7 +74,7 @@ export function inspectVNode<T extends SharedVNode>(
6974
key: vnode.key || null,
7075
hooks: supportsHooks ? hooks : !supportsHooks && hasHooks ? [] : null,
7176
id,
72-
name: bindings.getDisplayName(vnode, config),
77+
name: getSignalTextName(bindings.getDisplayName(vnode, config)),
7378
props,
7479
state,
7580
// TODO: We're not using this information anywhere yet

src/adapter/shared/serialize.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RendererConfig } from "./renderer";
22
import type { ObjPath } from "../renderer";
33
import type { PreactBindings, SharedVNode } from "./bindings";
4+
import type { Signal } from "@preact/signals";
45

56
export interface SerializedVNode {
67
type: "vnode";
@@ -22,6 +23,19 @@ export function serializeVNode<T extends SharedVNode>(
2223
return null;
2324
}
2425

26+
export function isSignal(x: any): x is Signal {
27+
return (
28+
x !== null &&
29+
typeof x === "object" &&
30+
typeof x.peek === "function" &&
31+
"value" in x
32+
);
33+
}
34+
35+
export function isReadOnlySignal(signal: Signal): boolean {
36+
return (signal as any)._r === true;
37+
}
38+
2539
export function jsonify(
2640
data: any,
2741
getVNode: (x: any) => SerializedVNode | null,
@@ -46,6 +60,14 @@ export function jsonify(
4660
const vnode = getVNode(data);
4761
if (vnode != null) return vnode;
4862

63+
if (isSignal(data)) {
64+
return {
65+
type: "signal",
66+
name: isReadOnlySignal(data) ? "computed Signal" : "Signal",
67+
value: jsonify(data.peek(), getVNode, seen),
68+
};
69+
}
70+
4971
if (Array.isArray(data)) {
5072
return data.map(x => jsonify(x, getVNode, seen));
5173
}
@@ -134,6 +156,11 @@ export function setInCopy<T = any>(
134156
): T {
135157
if (idx >= path.length) return value;
136158

159+
// Signals bypass everything
160+
if (path[path.length - 1] === "value" && maybeSetSignal(obj, path, value)) {
161+
return obj;
162+
}
163+
137164
const updated = clone(obj);
138165
if (obj instanceof Set) {
139166
const oldValue = Array.from(obj)[+path[idx]];
@@ -179,6 +206,24 @@ export function setIn(obj: Record<string, any>, path: ObjPath, value: any) {
179206
}
180207
}
181208

209+
export function maybeSetSignal(
210+
obj: Record<string, any>,
211+
path: ObjPath,
212+
value: any,
213+
) {
214+
let current: any = obj;
215+
for (let i = 0; i < path.length; i++) {
216+
if (isSignal(current)) {
217+
current.value = value;
218+
return true;
219+
}
220+
221+
current = current[path[i]];
222+
}
223+
224+
return false;
225+
}
226+
182227
export function hasIn(obj: Record<string, any>, path: ObjPath) {
183228
let item = obj;
184229
for (let i = 0; i < path.length; i++) {

src/adapter/shared/traverse.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getDiffType, recordComponentStats } from "./stats";
1919
import { measureUpdate } from "../adapter/highlightUpdates";
2020
import { PreactBindings, SharedVNode } from "./bindings";
2121
import { VNodeTimings } from "./timings";
22+
import { getSignalTextName } from "./utils";
2223

2324
function getHocName(name: string) {
2425
const idx = name.indexOf("(");
@@ -147,16 +148,23 @@ export function shouldFilter<T extends SharedVNode>(
147148
return false;
148149
} else if (bindings.isElement(vnode) && filters.type.has("dom")) {
149150
return true;
150-
} else if (filters.type.has("hoc")) {
151+
}
152+
153+
if (filters.type.has("hoc")) {
151154
const name = bindings.getDisplayName(vnode, config);
152155

153156
if (name.indexOf("(") > -1 && !name.startsWith("ForwardRef")) {
154157
return true;
155158
}
156159
}
157160

161+
if (filters.type.has("textSignal")) {
162+
const name = getSignalTextName(bindings.getDisplayName(vnode, config));
163+
if (name === "__TextSignal") return true;
164+
}
165+
158166
if (filters.regex.length > 0) {
159-
const name = bindings.getDisplayName(vnode, config);
167+
const name = getSignalTextName(bindings.getDisplayName(vnode, config));
160168
return filters.regex.some(r => {
161169
// Regexes with a global flag are stateful in JS :((
162170
r.lastIndex = 0;
@@ -202,7 +210,7 @@ function mount<T extends SharedVNode>(
202210
const root = bindings.isRoot(vnode, config);
203211

204212
const skip = shouldFilter(vnode, filters, config, bindings);
205-
let name = bindings.getDisplayName(vnode, config);
213+
let name = getSignalTextName(bindings.getDisplayName(vnode, config));
206214

207215
if (filters.type.has("hoc")) {
208216
const hocName = getHocName(name);
@@ -433,7 +441,7 @@ function update<T extends SharedVNode>(
433441
return;
434442
}
435443

436-
const name = bindings.getDisplayName(vnode, config);
444+
const name = getSignalTextName(bindings.getDisplayName(vnode, config));
437445
if (filters.type.has("hoc")) {
438446
const res = detectHocs(commit, name, id, hocs);
439447
hocs = res.hocs;

src/adapter/shared/update.ts

Lines changed: 0 additions & 72 deletions
This file was deleted.

src/adapter/shared/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { PreactBindings, SharedVNode } from "./bindings";
22

3+
export function getSignalTextName(name: string) {
4+
return name === "_st" ? "__TextSignal" : name;
5+
}
6+
37
export function traverse<T extends SharedVNode>(
48
vnode: T,
59
fn: (vnode: T) => void,

src/view/components/DataInput/parseValue.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export function genPreview(v: any): string {
5050
return `Set(${v.entries.length}) ${truncate(genPreview(v.entries))})`;
5151
} else if (v.type === "map") {
5252
return `Map(${v.entries.length}) ${truncate(genPreview(v.entries))})`;
53+
} else if (v.type === "signal") {
54+
return ${v.name} (${truncate(genPreview(v.value))})`;
5355
}
5456

5557
if (Array.isArray(v)) {

src/view/components/elements/TreeBar.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ export function FilterPopup() {
141141
);
142142
const [filterHoc, setFilterHoc] = useState(store.filter.filterHoc.value);
143143
const [filterRoot, setFilterRoot] = useState(store.filter.filterRoot.value);
144+
const [filterTextSignal, setFilterTextSignal] = useState(
145+
store.filter.filterTextSignal.value,
146+
);
144147
const [filters, setFilters] = useState(store.filter.filters.value);
145148

146149
return (
@@ -153,6 +156,7 @@ export function FilterPopup() {
153156
store.filter.filterFragment.value = filterFragment;
154157
store.filter.filterRoot.value = filterRoot;
155158
store.filter.filterHoc.value = filterHoc;
159+
store.filter.filterTextSignal.value = filterTextSignal;
156160

157161
store.filter.filters.value = filters;
158162

@@ -180,6 +184,11 @@ export function FilterPopup() {
180184
onInput={checked => setFilterDom(checked)}
181185
checked={filterDom}
182186
/>
187+
<FilterCheck
188+
label="Text Signal nodes"
189+
onInput={checked => setFilterTextSignal(checked)}
190+
checked={filterTextSignal}
191+
/>
183192
{/* Custom user filters */}
184193
{filters.map((x, i) => {
185194
return (

0 commit comments

Comments
 (0)