@excom/kit-devtools 0.1.0
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/.rush/temp/chunked-rush-logs/kit-devtools.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +1 -0
- package/package.json +38 -0
- package/rush-logs/kit-devtools.apply-exports.cache.log +1 -0
- package/rush-logs/kit-devtools.apply-exports.log +1 -0
- package/src/hook.ts +193 -0
- package/support/tests/hook.test.ts +307 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
package/config/rig.json
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/hook";
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/kit-devtools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "kit-devtools library",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@excom/kit-logger": "^0.1.0",
|
|
12
|
+
"@excom/kit-utils": "^0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@excom/heft-rig": "^0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"url": "excom-dev/nucleus",
|
|
20
|
+
"directory": "packages/kit-devtools"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/kit-devtools/support/docs/README.md",
|
|
23
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"kit-devtools"
|
|
26
|
+
],
|
|
27
|
+
"excom": {
|
|
28
|
+
"documented": false,
|
|
29
|
+
"packageType": "library"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
33
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
34
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
35
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
36
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
package/src/hook.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { toJsonSafe } from "@excom/kit-utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared DevTools hook (Neutron lifecycles, Quark orchestration, …).
|
|
5
|
+
* Shipped always; dormant until `__NUCLEUS_DEVTOOLS_HOOK__` is set
|
|
6
|
+
* (extension at `document_start`, or `attachDevtools()`).
|
|
7
|
+
*
|
|
8
|
+
* publicize(["neutron", "effect"], { weakElement, tag, effect })
|
|
9
|
+
* publicize(["quark", "apply"], { weakElement, tag, selector, key })
|
|
10
|
+
*
|
|
11
|
+
* Each producer `registerRenderer`s an inspector; `hook.inject(renderer)`
|
|
12
|
+
* once per hook, keyed by `renderer.kind`.
|
|
13
|
+
*
|
|
14
|
+
* No element refs here. Payloads are WeakRef + primitives.
|
|
15
|
+
* `inspect()` snapshots on demand; don't keep the live element.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const NUCLEUS_DEVTOOLS_HOOK_KEY = "__NUCLEUS_DEVTOOLS_HOOK__";
|
|
19
|
+
export const NUCLEUS_DEVTOOLS_HOOK_VERSION = 1 as const;
|
|
20
|
+
|
|
21
|
+
/** Hierarchical identifying tokens, e.g. `["neutron", "constructed"]`. */
|
|
22
|
+
export type PublicizePath = readonly string[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Publication metadata. Callers should pass WeakRef + primitives only.
|
|
26
|
+
* Never hard element / instance references.
|
|
27
|
+
*/
|
|
28
|
+
export type PublicizeMeta = Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
/** Which producer a renderer inspects for. */
|
|
31
|
+
export type DevtoolsRendererKind = "neutron" | "quark";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* On-demand inspector registered by a producer. `inspect` returns a
|
|
35
|
+
* JSON-safe snapshot for the element, or `null` when the producer has
|
|
36
|
+
* nothing to say about it.
|
|
37
|
+
*/
|
|
38
|
+
export type DevtoolsRenderer = {
|
|
39
|
+
version: typeof NUCLEUS_DEVTOOLS_HOOK_VERSION;
|
|
40
|
+
kind: DevtoolsRendererKind;
|
|
41
|
+
inspect: (el: Element) => Record<string, unknown> | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Global hook installed by a DevTools consumer.
|
|
46
|
+
* `publicize` mirrors the producer API: same `(path, meta)` signature.
|
|
47
|
+
* `inject` receives one renderer per producer kind.
|
|
48
|
+
*/
|
|
49
|
+
export type DevtoolsHook = {
|
|
50
|
+
version: typeof NUCLEUS_DEVTOOLS_HOOK_VERSION;
|
|
51
|
+
inject?: (renderer: DevtoolsRenderer) => void;
|
|
52
|
+
publicize?: (path: PublicizePath, meta: PublicizeMeta) => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
declare global {
|
|
56
|
+
var __NUCLEUS_DEVTOOLS_HOOK__: DevtoolsHook | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getDevtoolsHook = (): DevtoolsHook | undefined =>
|
|
60
|
+
(globalThis as Record<string, unknown>)[NUCLEUS_DEVTOOLS_HOOK_KEY] as
|
|
61
|
+
| DevtoolsHook
|
|
62
|
+
| undefined;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prefix / wildcard match for subscriber filtering.
|
|
66
|
+
* `["neutron"]` matches `["neutron", "constructed"]`.
|
|
67
|
+
* `"*"` matches any single token at that position.
|
|
68
|
+
*/
|
|
69
|
+
export const pathMatches = (
|
|
70
|
+
path: PublicizePath,
|
|
71
|
+
pattern: PublicizePath
|
|
72
|
+
): boolean =>
|
|
73
|
+
pattern.length <= path.length &&
|
|
74
|
+
pattern.every((token, i) => token === "*" || token === path[i]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Serializers for DevTools payloads (see `toJsonSafe`). Only plain data
|
|
78
|
+
* crosses into the extension (avoids cross-realm Object pitfalls).
|
|
79
|
+
* Non-POJOs (Promise, Map, …) become `{ $constructor: "Promise" }`.
|
|
80
|
+
*/
|
|
81
|
+
export const DEVTOOLS_SERIALIZERS: Record<string, (value: any) => unknown> = {
|
|
82
|
+
function: () => ({ $constructor: "Function" }),
|
|
83
|
+
symbol: (value: symbol) => ({
|
|
84
|
+
$constructor: "Symbol",
|
|
85
|
+
description: value.description ?? null,
|
|
86
|
+
}),
|
|
87
|
+
WeakRef: (value: WeakRef<WeakKey>) => {
|
|
88
|
+
const deref = value.deref();
|
|
89
|
+
return deref === undefined ? null : deref;
|
|
90
|
+
},
|
|
91
|
+
Node: (value: Node) => ({ $node: value.nodeName }),
|
|
92
|
+
Element: (value: Element) => ({
|
|
93
|
+
$element: value.localName ?? value.nodeName,
|
|
94
|
+
id: value.id || null,
|
|
95
|
+
}),
|
|
96
|
+
UnknownObject: (value: unknown) => ({
|
|
97
|
+
$constructor:
|
|
98
|
+
(value as object).constructor?.name ||
|
|
99
|
+
Object.prototype.toString.call(value),
|
|
100
|
+
}),
|
|
101
|
+
Circular: () => ({ $constructor: "Circular" }),
|
|
102
|
+
Failed: () => ({ $unserializable: true }),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** JSON-safe clone using the DevTools serializers. */
|
|
106
|
+
export const toDevtoolsJson = (value: unknown): unknown =>
|
|
107
|
+
toJsonSafe(value, DEVTOOLS_SERIALIZERS);
|
|
108
|
+
|
|
109
|
+
const renderers = new Map<DevtoolsRendererKind, DevtoolsRenderer>();
|
|
110
|
+
/** Renderer kinds already injected into a given hook object. */
|
|
111
|
+
const injected = new WeakMap<DevtoolsHook, Set<DevtoolsRendererKind>>();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Register (or replace) a producer's renderer. Injected into the current
|
|
115
|
+
* hook immediately when one is installed, and into any hook attached later.
|
|
116
|
+
*/
|
|
117
|
+
export const registerRenderer = (renderer: DevtoolsRenderer): void => {
|
|
118
|
+
renderers.set(renderer.kind, renderer);
|
|
119
|
+
const hook = getDevtoolsHook();
|
|
120
|
+
if (hook) injected.get(hook)?.delete(renderer.kind);
|
|
121
|
+
injectRenderersIfNeeded();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const getRenderer = (
|
|
125
|
+
kind: DevtoolsRendererKind
|
|
126
|
+
): DevtoolsRenderer | undefined => renderers.get(kind);
|
|
127
|
+
|
|
128
|
+
/** Hand every registered renderer to the hook, once per hook object. */
|
|
129
|
+
export const injectRenderersIfNeeded = (): void => {
|
|
130
|
+
const hook = getDevtoolsHook();
|
|
131
|
+
if (!hook?.inject) return;
|
|
132
|
+
let done = injected.get(hook);
|
|
133
|
+
if (!done) {
|
|
134
|
+
done = new Set();
|
|
135
|
+
injected.set(hook, done);
|
|
136
|
+
}
|
|
137
|
+
for (const renderer of renderers.values()) {
|
|
138
|
+
if (done.has(renderer.kind)) continue;
|
|
139
|
+
done.add(renderer.kind);
|
|
140
|
+
hook.inject(renderer);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Install (or replace) the global hook and (re)inject every renderer.
|
|
146
|
+
* Used by tests and by late-attaching consumers (e.g. a bookmarklet).
|
|
147
|
+
* Chrome extensions should set the global at `document_start` instead.
|
|
148
|
+
*
|
|
149
|
+
* Subscriber mirrors producer:
|
|
150
|
+
* attachDevtools({
|
|
151
|
+
* version: 1,
|
|
152
|
+
* publicize: (path, meta) => { … },
|
|
153
|
+
* })
|
|
154
|
+
*/
|
|
155
|
+
export const attachDevtools = (hook: DevtoolsHook): void => {
|
|
156
|
+
(globalThis as Record<string, unknown>)[NUCLEUS_DEVTOOLS_HOOK_KEY] = hook;
|
|
157
|
+
injected.delete(hook);
|
|
158
|
+
injectRenderersIfNeeded();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Event a consumer dispatches on `globalThis` after installing the hook
|
|
163
|
+
* global itself, late, without access to `attachDevtools` (an injected
|
|
164
|
+
* agent-tools bundle). Producers answer by injecting their renderers.
|
|
165
|
+
*/
|
|
166
|
+
export const NUCLEUS_DEVTOOLS_ATTACH_EVENT = "nucleus-devtools-attach";
|
|
167
|
+
|
|
168
|
+
if (typeof globalThis.addEventListener === "function") {
|
|
169
|
+
globalThis.addEventListener(NUCLEUS_DEVTOOLS_ATTACH_EVENT, () =>
|
|
170
|
+
injectRenderersIfNeeded()
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Publish a DevTools event. No-op when no hook is installed.
|
|
176
|
+
* Meta (except `weakElement`) is JSON-cloned in-page before crossing to the hook.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* publicize(["neutron", "constructed"], { weakElement, tag })
|
|
180
|
+
* publicize(["quark", "apply"], { weakElement, tag, selector, key })
|
|
181
|
+
*/
|
|
182
|
+
export const publicize = (
|
|
183
|
+
path: PublicizePath,
|
|
184
|
+
meta: PublicizeMeta = {}
|
|
185
|
+
): void => {
|
|
186
|
+
const hook = getDevtoolsHook();
|
|
187
|
+
if (!hook?.publicize) return;
|
|
188
|
+
const { weakElement, ...rest } = meta;
|
|
189
|
+
hook.publicize(path, {
|
|
190
|
+
...(toDevtoolsJson(rest) as PublicizeMeta),
|
|
191
|
+
...(weakElement ? { weakElement } : {}),
|
|
192
|
+
});
|
|
193
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachDevtools,
|
|
3
|
+
DEVTOOLS_SERIALIZERS,
|
|
4
|
+
getDevtoolsHook,
|
|
5
|
+
getRenderer,
|
|
6
|
+
injectRenderersIfNeeded,
|
|
7
|
+
NUCLEUS_DEVTOOLS_ATTACH_EVENT,
|
|
8
|
+
NUCLEUS_DEVTOOLS_HOOK_KEY,
|
|
9
|
+
NUCLEUS_DEVTOOLS_HOOK_VERSION,
|
|
10
|
+
pathMatches,
|
|
11
|
+
publicize,
|
|
12
|
+
registerRenderer,
|
|
13
|
+
toDevtoolsJson,
|
|
14
|
+
type DevtoolsHook,
|
|
15
|
+
type DevtoolsRenderer,
|
|
16
|
+
} from "../../index";
|
|
17
|
+
import {
|
|
18
|
+
afterEach,
|
|
19
|
+
describe,
|
|
20
|
+
expect,
|
|
21
|
+
it,
|
|
22
|
+
vi,
|
|
23
|
+
} from "@excom/heft-rig/node_modules/vitest";
|
|
24
|
+
|
|
25
|
+
const uninstallHook = () => {
|
|
26
|
+
delete (globalThis as Record<string, unknown>)[NUCLEUS_DEVTOOLS_HOOK_KEY];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const makeRenderer = (
|
|
30
|
+
kind: DevtoolsRenderer["kind"],
|
|
31
|
+
inspect: DevtoolsRenderer["inspect"] = () => null
|
|
32
|
+
): DevtoolsRenderer => ({
|
|
33
|
+
version: NUCLEUS_DEVTOOLS_HOOK_VERSION,
|
|
34
|
+
kind,
|
|
35
|
+
inspect,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("hook basics", () => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
uninstallHook();
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("exposes the global key and version", () => {
|
|
45
|
+
expect(NUCLEUS_DEVTOOLS_HOOK_KEY).toBe("__NUCLEUS_DEVTOOLS_HOOK__");
|
|
46
|
+
expect(NUCLEUS_DEVTOOLS_HOOK_VERSION).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("getDevtoolsHook reads the global slot", () => {
|
|
50
|
+
expect(getDevtoolsHook()).toBeUndefined();
|
|
51
|
+
const hook: DevtoolsHook = { version: 1 };
|
|
52
|
+
attachDevtools(hook);
|
|
53
|
+
expect(getDevtoolsHook()).toBe(hook);
|
|
54
|
+
expect(globalThis.__NUCLEUS_DEVTOOLS_HOOK__).toBe(hook);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("pathMatches", () => {
|
|
59
|
+
it("matches an exact path", () => {
|
|
60
|
+
expect(pathMatches(["neutron", "constructed"], ["neutron", "constructed"])).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("matches a prefix pattern", () => {
|
|
64
|
+
expect(pathMatches(["neutron", "constructed"], ["neutron"])).toBe(true);
|
|
65
|
+
expect(pathMatches(["neutron", "constructed"], [])).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("treats * as a single-token wildcard", () => {
|
|
69
|
+
expect(pathMatches(["quark", "apply"], ["*", "apply"])).toBe(true);
|
|
70
|
+
expect(pathMatches(["quark", "apply"], ["quark", "*"])).toBe(true);
|
|
71
|
+
expect(pathMatches(["quark", "apply"], ["*", "*", "*"])).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects mismatching tokens and longer patterns", () => {
|
|
75
|
+
expect(pathMatches(["quark", "apply"], ["neutron"])).toBe(false);
|
|
76
|
+
expect(pathMatches(["quark"], ["quark", "apply"])).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("toDevtoolsJson / DEVTOOLS_SERIALIZERS", () => {
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
document.body.innerHTML = "";
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("serializes functions", () => {
|
|
86
|
+
expect(toDevtoolsJson({ fn: () => 1 })).toEqual({
|
|
87
|
+
fn: { $constructor: "Function" },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("serializes symbols with and without a description", () => {
|
|
92
|
+
expect(toDevtoolsJson({ s: Symbol("tag") })).toEqual({
|
|
93
|
+
s: { $constructor: "Symbol", description: "tag" },
|
|
94
|
+
});
|
|
95
|
+
expect(DEVTOOLS_SERIALIZERS.symbol(Symbol())).toEqual({
|
|
96
|
+
$constructor: "Symbol",
|
|
97
|
+
description: null,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("serializes elements with id, without id, and plain nodes", () => {
|
|
102
|
+
const withId = document.createElement("my-widget");
|
|
103
|
+
withId.id = "one";
|
|
104
|
+
const noId = document.createElement("section");
|
|
105
|
+
const text = document.createTextNode("hi");
|
|
106
|
+
expect(toDevtoolsJson({ withId, noId, text })).toEqual({
|
|
107
|
+
withId: { $element: "my-widget", id: "one" },
|
|
108
|
+
noId: { $element: "section", id: null },
|
|
109
|
+
text: { $node: "#text" },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("derefs WeakRefs and nulls dead ones", () => {
|
|
114
|
+
const el = document.createElement("p");
|
|
115
|
+
el.id = "weak";
|
|
116
|
+
expect(toDevtoolsJson({ ref: new WeakRef(el) })).toEqual({
|
|
117
|
+
ref: { $element: "p", id: "weak" },
|
|
118
|
+
});
|
|
119
|
+
const dead = { deref: () => undefined } as unknown as WeakRef<object>;
|
|
120
|
+
expect(DEVTOOLS_SERIALIZERS.WeakRef(dead)).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("tags non-POJOs with their constructor name", () => {
|
|
124
|
+
class Thing {}
|
|
125
|
+
expect(toDevtoolsJson({ m: new Map(), p: Promise.resolve(), t: new Thing() })).toEqual({
|
|
126
|
+
m: { $constructor: "Map" },
|
|
127
|
+
p: { $constructor: "Promise" },
|
|
128
|
+
t: { $constructor: "Thing" },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("falls back to Object.prototype.toString for nameless constructors", () => {
|
|
133
|
+
const anon = Object.create({ constructor: { name: "" } });
|
|
134
|
+
expect(DEVTOOLS_SERIALIZERS.UnknownObject(anon)).toEqual({
|
|
135
|
+
$constructor: "[object Object]",
|
|
136
|
+
});
|
|
137
|
+
const noCtor = Object.create({});
|
|
138
|
+
Object.defineProperty(noCtor, "constructor", { value: undefined });
|
|
139
|
+
expect(DEVTOOLS_SERIALIZERS.UnknownObject(noCtor)).toEqual({
|
|
140
|
+
$constructor: "[object Object]",
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("marks circular references", () => {
|
|
145
|
+
const obj: Record<string, unknown> = { name: "loop" };
|
|
146
|
+
obj.self = obj;
|
|
147
|
+
expect(toDevtoolsJson(obj)).toEqual({
|
|
148
|
+
name: "loop",
|
|
149
|
+
self: { $constructor: "Circular" },
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("marks values that cannot be serialized", () => {
|
|
154
|
+
const broken = {
|
|
155
|
+
toJSON() {
|
|
156
|
+
throw new Error("nope");
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
expect(toDevtoolsJson(broken)).toEqual({ $unserializable: true });
|
|
160
|
+
expect(DEVTOOLS_SERIALIZERS.Failed()).toEqual({ $unserializable: true });
|
|
161
|
+
expect(DEVTOOLS_SERIALIZERS.Circular()).toEqual({ $constructor: "Circular" });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("passes primitives, arrays and POJOs through", () => {
|
|
165
|
+
expect(toDevtoolsJson({ a: [1, "two", null, { b: true }] })).toEqual({
|
|
166
|
+
a: [1, "two", null, { b: true }],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("publicize", () => {
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
uninstallHook();
|
|
174
|
+
document.body.innerHTML = "";
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("is a no-op without a hook", () => {
|
|
178
|
+
expect(() => publicize(["neutron", "constructed"], { tag: "x" })).not.toThrow();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("is a no-op when the hook has no publicize", () => {
|
|
182
|
+
attachDevtools({ version: 1 });
|
|
183
|
+
expect(() => publicize(["neutron", "constructed"])).not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("clones meta and forwards weakElement untouched", () => {
|
|
187
|
+
const received: unknown[] = [];
|
|
188
|
+
attachDevtools({
|
|
189
|
+
version: 1,
|
|
190
|
+
publicize: (path, meta) => received.push([path, meta]),
|
|
191
|
+
});
|
|
192
|
+
const el = document.createElement("my-el");
|
|
193
|
+
const weakElement = new WeakRef(el);
|
|
194
|
+
const effect = () => {};
|
|
195
|
+
publicize(["neutron", "effect"], { weakElement, tag: "my-el", effect, el });
|
|
196
|
+
expect(received).toHaveLength(1);
|
|
197
|
+
const [path, meta] = received[0] as [string[], Record<string, unknown>];
|
|
198
|
+
expect(path).toEqual(["neutron", "effect"]);
|
|
199
|
+
expect(meta.weakElement).toBe(weakElement);
|
|
200
|
+
expect(meta.tag).toBe("my-el");
|
|
201
|
+
expect(meta.effect).toEqual({ $constructor: "Function" });
|
|
202
|
+
expect(meta.el).toEqual({ $element: "my-el", id: null });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("omits weakElement when not supplied and defaults meta to {}", () => {
|
|
206
|
+
const spy = vi.fn();
|
|
207
|
+
attachDevtools({ version: 1, publicize: spy });
|
|
208
|
+
publicize(["quark", "apply"]);
|
|
209
|
+
expect(spy).toHaveBeenCalledWith(["quark", "apply"], {});
|
|
210
|
+
expect(spy.mock.calls[0][1]).not.toHaveProperty("weakElement");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("renderer registry", () => {
|
|
215
|
+
afterEach(() => {
|
|
216
|
+
uninstallHook();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("stores renderers without a hook and exposes them via getRenderer", () => {
|
|
220
|
+
const neutron = makeRenderer("neutron", () => ({ kind: "n" }));
|
|
221
|
+
registerRenderer(neutron);
|
|
222
|
+
expect(getRenderer("neutron")).toBe(neutron);
|
|
223
|
+
expect(getRenderer("neutron")!.inspect(document.body)).toEqual({ kind: "n" });
|
|
224
|
+
// No hook installed: injecting is a silent no-op.
|
|
225
|
+
expect(() => injectRenderersIfNeeded()).not.toThrow();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("injects registered renderers when a hook is attached, once per hook", () => {
|
|
229
|
+
const neutron = makeRenderer("neutron");
|
|
230
|
+
const quark = makeRenderer("quark");
|
|
231
|
+
registerRenderer(neutron);
|
|
232
|
+
registerRenderer(quark);
|
|
233
|
+
|
|
234
|
+
const inject = vi.fn();
|
|
235
|
+
const hook: DevtoolsHook = { version: 1, inject };
|
|
236
|
+
attachDevtools(hook);
|
|
237
|
+
expect(inject).toHaveBeenCalledTimes(2);
|
|
238
|
+
expect(inject.mock.calls.map((c) => c[0].kind).sort()).toEqual(["neutron", "quark"]);
|
|
239
|
+
|
|
240
|
+
// Repeated injection requests are de-duplicated per hook object.
|
|
241
|
+
injectRenderersIfNeeded();
|
|
242
|
+
injectRenderersIfNeeded();
|
|
243
|
+
expect(inject).toHaveBeenCalledTimes(2);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("re-injects everything when attachDevtools is called again with the same hook", () => {
|
|
247
|
+
registerRenderer(makeRenderer("neutron"));
|
|
248
|
+
registerRenderer(makeRenderer("quark"));
|
|
249
|
+
const inject = vi.fn();
|
|
250
|
+
const hook: DevtoolsHook = { version: 1, inject };
|
|
251
|
+
attachDevtools(hook);
|
|
252
|
+
attachDevtools(hook);
|
|
253
|
+
expect(inject).toHaveBeenCalledTimes(4);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("injects into a different hook object independently", () => {
|
|
257
|
+
registerRenderer(makeRenderer("neutron"));
|
|
258
|
+
const first = vi.fn();
|
|
259
|
+
const second = vi.fn();
|
|
260
|
+
attachDevtools({ version: 1, inject: first });
|
|
261
|
+
attachDevtools({ version: 1, inject: second });
|
|
262
|
+
expect(first).toHaveBeenCalled();
|
|
263
|
+
expect(second).toHaveBeenCalled();
|
|
264
|
+
expect(getDevtoolsHook()!.inject).toBe(second);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("registering a renderer while a hook is installed injects it immediately", () => {
|
|
268
|
+
const inject = vi.fn();
|
|
269
|
+
attachDevtools({ version: 1, inject });
|
|
270
|
+
const before = inject.mock.calls.length;
|
|
271
|
+
const replacement = makeRenderer("quark", () => ({ replaced: true }));
|
|
272
|
+
registerRenderer(replacement);
|
|
273
|
+
expect(inject).toHaveBeenCalledTimes(before + 1);
|
|
274
|
+
expect(inject).toHaveBeenLastCalledWith(replacement);
|
|
275
|
+
expect(getRenderer("quark")).toBe(replacement);
|
|
276
|
+
|
|
277
|
+
// Re-registering the same kind replaces and re-injects it.
|
|
278
|
+
const again = makeRenderer("quark");
|
|
279
|
+
registerRenderer(again);
|
|
280
|
+
expect(inject).toHaveBeenLastCalledWith(again);
|
|
281
|
+
expect(inject).toHaveBeenCalledTimes(before + 2);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("does nothing when the hook has no inject", () => {
|
|
285
|
+
const hook: DevtoolsHook = { version: 1 };
|
|
286
|
+
registerRenderer(makeRenderer("neutron"));
|
|
287
|
+
expect(() => attachDevtools(hook)).not.toThrow();
|
|
288
|
+
expect(() => registerRenderer(makeRenderer("quark"))).not.toThrow();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("injects into a hook installed by hand once the attach event fires", () => {
|
|
292
|
+
registerRenderer(makeRenderer("neutron"));
|
|
293
|
+
const inject = vi.fn();
|
|
294
|
+
// an injected bundle sets the global itself, it has no attachDevtools
|
|
295
|
+
(globalThis as Record<string, unknown>)[NUCLEUS_DEVTOOLS_HOOK_KEY] = {
|
|
296
|
+
version: 1,
|
|
297
|
+
inject,
|
|
298
|
+
} satisfies DevtoolsHook;
|
|
299
|
+
expect(inject).not.toHaveBeenCalled();
|
|
300
|
+
globalThis.dispatchEvent(new Event(NUCLEUS_DEVTOOLS_ATTACH_EVENT));
|
|
301
|
+
expect(inject).toHaveBeenCalledWith(getRenderer("neutron"));
|
|
302
|
+
// idempotent per hook object
|
|
303
|
+
const count = inject.mock.calls.length;
|
|
304
|
+
globalThis.dispatchEvent(new Event(NUCLEUS_DEVTOOLS_ATTACH_EVENT));
|
|
305
|
+
expect(inject).toHaveBeenCalledTimes(count);
|
|
306
|
+
});
|
|
307
|
+
});
|